@link-assistant/hive-mind 1.34.8 ā 1.35.1
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 +14 -0
- package/package.json +1 -1
- package/src/github.lib.mjs +16 -11
- package/src/interactive-mode.lib.mjs +248 -0
- package/src/limits.lib.mjs +100 -12
- package/src/model-info.lib.mjs +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.35.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Fix misleading "Retry after: 0s" message in /limits command when Claude Usage API returns 429. Now shows "Try again later." for zero/missing retry-after values, or proper reset time format (e.g., "Resets in 5m (Mar 19, 8:00pm UTC)") for meaningful values. Also caches 429 errors to prevent repeated requests to rate-limited endpoint, and adds full request/response verbose logging for debugging.
|
|
8
|
+
|
|
9
|
+
improve Solution Draft Log comment formatting for better readability (issue #1448)
|
|
10
|
+
|
|
11
|
+
## 1.35.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- 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%
|
|
16
|
+
|
|
3
17
|
## 1.34.8
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/package.json
CHANGED
package/src/github.lib.mjs
CHANGED
|
@@ -22,7 +22,7 @@ const buildCostInfoString = (totalCostUSD, anthropicTotalCostUSD, pricingInfo) =
|
|
|
22
22
|
const hasPricing = pricingInfo && (pricingInfo.modelName || pricingInfo.tokenUsage || pricingInfo.isFreeModel || pricingInfo.isOpencodeFreeModel);
|
|
23
23
|
const hasOpencodeCost = pricingInfo?.opencodeCost !== null && pricingInfo?.opencodeCost !== undefined;
|
|
24
24
|
if (!hasPublic && !hasAnthropic && !hasPricing && !hasOpencodeCost) return '';
|
|
25
|
-
let costInfo = '\n\nš° **Cost estimation:**';
|
|
25
|
+
let costInfo = '\n\n### š° **Cost estimation:**';
|
|
26
26
|
if (pricingInfo?.modelName) {
|
|
27
27
|
costInfo += `\n- Model: ${pricingInfo.modelName}`;
|
|
28
28
|
if (pricingInfo.provider) costInfo += `\n- Provider: ${pricingInfo.provider}`;
|
|
@@ -557,7 +557,7 @@ ${logContent}
|
|
|
557
557
|
logComment = `## ā ļø Solution Draft Finished with Errors
|
|
558
558
|
This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${modelInfoString}
|
|
559
559
|
|
|
560
|
-
**Note**: The session encountered errors during execution, but some work may have been completed. Please review the changes carefully.
|
|
560
|
+
> **Note**: The session encountered errors during execution, but some work may have been completed. Please review the changes carefully.
|
|
561
561
|
|
|
562
562
|
<details>
|
|
563
563
|
<summary>Click to expand solution draft log (${Math.round(logStats.size / 1024)}KB)</summary>
|
|
@@ -714,8 +714,8 @@ ${resumeCommand}
|
|
|
714
714
|
|
|
715
715
|
logUploadComment += `${modelInfoString}
|
|
716
716
|
|
|
717
|
-
š **Execution log uploaded as ${uploadTypeLabel}${chunkInfo}** (${Math.round(logStats.size / 1024)}KB)
|
|
718
|
-
|
|
717
|
+
### š **Execution log uploaded as ${uploadTypeLabel}${chunkInfo}** (${Math.round(logStats.size / 1024)}KB)
|
|
718
|
+
- [View complete execution log](${logUrl})
|
|
719
719
|
|
|
720
720
|
---
|
|
721
721
|
*This session was interrupted due to usage limits. You can resume once the limit resets.*`;
|
|
@@ -726,8 +726,10 @@ The automated solution draft encountered an error:
|
|
|
726
726
|
\`\`\`
|
|
727
727
|
${errorMessage}
|
|
728
728
|
\`\`\`${modelInfoString}
|
|
729
|
-
|
|
730
|
-
|
|
729
|
+
|
|
730
|
+
### š **Failure log uploaded as ${uploadTypeLabel}${chunkInfo}** (${Math.round(logStats.size / 1024)}KB)
|
|
731
|
+
- [View complete failure log](${logUrl})
|
|
732
|
+
|
|
731
733
|
---
|
|
732
734
|
*Now working session is ended, feel free to review and add any feedback on the solution draft.*`;
|
|
733
735
|
} else if (errorDuringExecution) {
|
|
@@ -736,10 +738,11 @@ ${errorMessage}
|
|
|
736
738
|
logUploadComment = `## ā ļø Solution Draft Finished with Errors
|
|
737
739
|
This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${modelInfoString}
|
|
738
740
|
|
|
739
|
-
**Note**: The session encountered errors during execution, but some work may have been completed. Please review the changes carefully.
|
|
741
|
+
> **Note**: The session encountered errors during execution, but some work may have been completed. Please review the changes carefully.
|
|
742
|
+
|
|
743
|
+
### š **Log file uploaded as ${uploadTypeLabel}${chunkInfo}** (${Math.round(logStats.size / 1024)}KB)
|
|
744
|
+
- [View complete solution draft log](${logUrl})
|
|
740
745
|
|
|
741
|
-
š **Log file uploaded as ${uploadTypeLabel}${chunkInfo}** (${Math.round(logStats.size / 1024)}KB)
|
|
742
|
-
š [View complete solution draft log](${logUrl})
|
|
743
746
|
---
|
|
744
747
|
*Now working session is ended, feel free to review and add any feedback on the solution draft.*`;
|
|
745
748
|
} else {
|
|
@@ -761,8 +764,10 @@ This log file contains the complete execution trace of the AI ${targetType === '
|
|
|
761
764
|
}
|
|
762
765
|
logUploadComment = `## ${title}
|
|
763
766
|
This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${modelInfoString}
|
|
764
|
-
${sessionNote}
|
|
765
|
-
|
|
767
|
+
${sessionNote}
|
|
768
|
+
### š **Log file uploaded as ${uploadTypeLabel}${chunkInfo}** (${Math.round(logStats.size / 1024)}KB)
|
|
769
|
+
- [View complete solution draft log](${logUrl})
|
|
770
|
+
|
|
766
771
|
---
|
|
767
772
|
*Now working session is ended, feel free to review and add any feedback on the solution draft.*`;
|
|
768
773
|
}
|
|
@@ -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
|
};
|
package/src/limits.lib.mjs
CHANGED
|
@@ -61,6 +61,63 @@ async function readCredentials(credentialsPath = DEFAULT_CREDENTIALS_PATH, verbo
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Format a retry-after value into a user-friendly message.
|
|
66
|
+
* The retry-after header can be either a number of seconds or an HTTP-date.
|
|
67
|
+
* Handles edge cases like 0, missing, or negative values gracefully.
|
|
68
|
+
*
|
|
69
|
+
* @param {string|null} retryAfter - Value of the retry-after header
|
|
70
|
+
* @returns {string} Formatted message part (e.g., " Resets in 2m 30s (Mar 19, 8:00pm UTC)" or " Try again later.")
|
|
71
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1446
|
|
72
|
+
*/
|
|
73
|
+
export function formatRetryAfterMessage(retryAfter) {
|
|
74
|
+
if (retryAfter === null || retryAfter === undefined) {
|
|
75
|
+
return ' Try again later.';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Try to parse as number of seconds first
|
|
79
|
+
const seconds = Number(retryAfter);
|
|
80
|
+
if (!Number.isNaN(seconds) && seconds > 0) {
|
|
81
|
+
// Calculate reset time from now + seconds
|
|
82
|
+
const resetAt = dayjs().add(seconds, 'second').utc();
|
|
83
|
+
const resetTimeStr = resetAt.format('MMM D, h:mma');
|
|
84
|
+
|
|
85
|
+
// Format relative time
|
|
86
|
+
const totalMinutes = Math.floor(seconds / 60);
|
|
87
|
+
const remainingSeconds = Math.round(seconds % 60);
|
|
88
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
89
|
+
const minutes = totalMinutes % 60;
|
|
90
|
+
|
|
91
|
+
let relativeStr;
|
|
92
|
+
if (hours > 0) {
|
|
93
|
+
relativeStr = `${hours}h ${minutes}m`;
|
|
94
|
+
} else if (minutes > 0) {
|
|
95
|
+
relativeStr = remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
|
|
96
|
+
} else {
|
|
97
|
+
relativeStr = `${remainingSeconds}s`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return ` Resets in ${relativeStr} (${resetTimeStr} UTC)`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Try to parse as HTTP-date (e.g., "Wed, 21 Oct 2015 07:28:00 GMT")
|
|
104
|
+
const retryDate = dayjs(retryAfter);
|
|
105
|
+
if (retryDate.isValid()) {
|
|
106
|
+
const diffMs = retryDate.diff(dayjs());
|
|
107
|
+
if (diffMs > 0) {
|
|
108
|
+
const totalMinutes = Math.floor(diffMs / (1000 * 60));
|
|
109
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
110
|
+
const minutes = totalMinutes % 60;
|
|
111
|
+
const relativeStr = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
|
112
|
+
const resetTimeStr = retryDate.utc().format('MMM D, h:mma');
|
|
113
|
+
return ` Resets in ${relativeStr} (${resetTimeStr} UTC)`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Fallback for 0, negative, or unparseable values - don't show misleading info
|
|
118
|
+
return ' Try again later.';
|
|
119
|
+
}
|
|
120
|
+
|
|
64
121
|
/**
|
|
65
122
|
* Format an ISO date string to a human-readable reset time using dayjs
|
|
66
123
|
*
|
|
@@ -534,31 +591,47 @@ export async function getClaudeUsageLimits(verbose = false, credentialsPath = DE
|
|
|
534
591
|
};
|
|
535
592
|
}
|
|
536
593
|
|
|
594
|
+
const requestHeaders = {
|
|
595
|
+
Accept: 'application/json',
|
|
596
|
+
'Content-Type': 'application/json',
|
|
597
|
+
'User-Agent': 'claude-code/2.0.55',
|
|
598
|
+
Authorization: `Bearer ${accessToken}`,
|
|
599
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
600
|
+
};
|
|
601
|
+
|
|
537
602
|
if (verbose) {
|
|
538
603
|
console.log('[VERBOSE] /limits fetching usage from API...');
|
|
604
|
+
console.log(`[VERBOSE] /limits API request: GET ${USAGE_API_ENDPOINT}`);
|
|
605
|
+
// Log request headers with sanitized Authorization (show only last 8 chars)
|
|
606
|
+
const sanitizedHeaders = { ...requestHeaders };
|
|
607
|
+
if (sanitizedHeaders.Authorization) {
|
|
608
|
+
const token = sanitizedHeaders.Authorization;
|
|
609
|
+
sanitizedHeaders.Authorization = `Bearer ...${token.slice(-8)}`;
|
|
610
|
+
}
|
|
611
|
+
console.log('[VERBOSE] /limits API request headers:', JSON.stringify(sanitizedHeaders, null, 2));
|
|
539
612
|
}
|
|
540
613
|
|
|
541
614
|
// Call the Anthropic OAuth usage API
|
|
542
615
|
const response = await fetch(USAGE_API_ENDPOINT, {
|
|
543
616
|
method: 'GET',
|
|
544
|
-
headers:
|
|
545
|
-
Accept: 'application/json',
|
|
546
|
-
'Content-Type': 'application/json',
|
|
547
|
-
'User-Agent': 'claude-code/2.0.55',
|
|
548
|
-
Authorization: `Bearer ${accessToken}`,
|
|
549
|
-
'anthropic-beta': 'oauth-2025-04-20',
|
|
550
|
-
},
|
|
617
|
+
headers: requestHeaders,
|
|
551
618
|
});
|
|
552
619
|
|
|
553
|
-
// Log HTTP response status for debugging (always, not just on error)
|
|
620
|
+
// Log HTTP response status and headers for debugging (always in verbose mode, not just on error)
|
|
554
621
|
if (verbose) {
|
|
555
622
|
console.log(`[VERBOSE] /limits API HTTP status: ${response.status} ${response.statusText}`);
|
|
623
|
+
// Log all response headers for debugging
|
|
624
|
+
const responseHeaders = {};
|
|
625
|
+
response.headers.forEach((value, key) => {
|
|
626
|
+
responseHeaders[key] = value;
|
|
627
|
+
});
|
|
628
|
+
console.log('[VERBOSE] /limits API response headers:', JSON.stringify(responseHeaders, null, 2));
|
|
556
629
|
}
|
|
557
630
|
|
|
558
631
|
if (!response.ok) {
|
|
559
632
|
const errorText = await response.text();
|
|
560
633
|
if (verbose) {
|
|
561
|
-
console.error('[VERBOSE] /limits API error:',
|
|
634
|
+
console.error('[VERBOSE] /limits API error body:', errorText);
|
|
562
635
|
}
|
|
563
636
|
|
|
564
637
|
// Check for specific error conditions
|
|
@@ -574,7 +647,7 @@ export async function getClaudeUsageLimits(verbose = false, credentialsPath = DE
|
|
|
574
647
|
const retryAfter = response.headers.get('retry-after');
|
|
575
648
|
return {
|
|
576
649
|
success: false,
|
|
577
|
-
error: `
|
|
650
|
+
error: `Claude Usage API access has reached rate limit.${formatRetryAfterMessage(retryAfter)}`,
|
|
578
651
|
};
|
|
579
652
|
}
|
|
580
653
|
|
|
@@ -587,7 +660,7 @@ export async function getClaudeUsageLimits(verbose = false, credentialsPath = DE
|
|
|
587
660
|
const data = await response.json();
|
|
588
661
|
|
|
589
662
|
if (verbose) {
|
|
590
|
-
console.log('[VERBOSE] /limits API response:', JSON.stringify(data, null, 2));
|
|
663
|
+
console.log('[VERBOSE] /limits API response body:', JSON.stringify(data, null, 2));
|
|
591
664
|
}
|
|
592
665
|
|
|
593
666
|
// Parse the API response
|
|
@@ -971,9 +1044,23 @@ export async function getCachedClaudeLimits(verbose = false) {
|
|
|
971
1044
|
if (verbose) console.log('[VERBOSE] /limits-cache: Using cached Claude limits (TTL: ' + Math.round(CACHE_TTL.USAGE_API / 60000) + ' minutes)');
|
|
972
1045
|
return cached;
|
|
973
1046
|
}
|
|
1047
|
+
// Also check if we have a cached rate-limit error to avoid hammering a 429'd endpoint
|
|
1048
|
+
const cachedError = cache.get('claude-rate-limited', CACHE_TTL.USAGE_API);
|
|
1049
|
+
if (cachedError) {
|
|
1050
|
+
if (verbose) console.log('[VERBOSE] /limits-cache: Using cached rate-limit error (avoiding repeated 429 requests)');
|
|
1051
|
+
return cachedError;
|
|
1052
|
+
}
|
|
974
1053
|
if (verbose) console.log('[VERBOSE] /limits-cache: Cache miss for Claude limits, fetching from API...');
|
|
975
1054
|
const result = await getClaudeUsageLimits(verbose);
|
|
976
|
-
if (result.success)
|
|
1055
|
+
if (result.success) {
|
|
1056
|
+
cache.set('claude', result, CACHE_TTL.USAGE_API);
|
|
1057
|
+
} else if (result.error && result.error.includes('Rate limited')) {
|
|
1058
|
+
// Cache rate-limit errors to prevent hammering the API
|
|
1059
|
+
// Use the same 20-minute TTL as successful responses
|
|
1060
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1446
|
|
1061
|
+
cache.set('claude-rate-limited', result, CACHE_TTL.USAGE_API);
|
|
1062
|
+
if (verbose) console.log('[VERBOSE] /limits-cache: Cached rate-limit error for ' + Math.round(CACHE_TTL.USAGE_API / 60000) + ' minutes');
|
|
1063
|
+
}
|
|
977
1064
|
return result;
|
|
978
1065
|
}
|
|
979
1066
|
|
|
@@ -1040,6 +1127,7 @@ export default {
|
|
|
1040
1127
|
getProgressBar,
|
|
1041
1128
|
calculateTimePassedPercentage,
|
|
1042
1129
|
formatUsageMessage,
|
|
1130
|
+
formatRetryAfterMessage,
|
|
1043
1131
|
// Threshold constants for progress bar visualization
|
|
1044
1132
|
DISPLAY_THRESHOLDS,
|
|
1045
1133
|
// Cache management
|
package/src/model-info.lib.mjs
CHANGED
|
@@ -179,7 +179,7 @@ export const buildModelInfoString = ({ requestedModel = null, tool = null, prici
|
|
|
179
179
|
|
|
180
180
|
if (!hasRequested && !hasModelsUsed && !hasModelInfo && !hasPricingModel) return '';
|
|
181
181
|
|
|
182
|
-
let info = '\n\nš¤ **Models used:**';
|
|
182
|
+
let info = '\n\n### š¤ **Models used:**';
|
|
183
183
|
|
|
184
184
|
// Display tool name
|
|
185
185
|
if (tool) {
|
|
@@ -201,26 +201,26 @@ export const buildModelInfoString = ({ requestedModel = null, tool = null, prici
|
|
|
201
201
|
|
|
202
202
|
// Build main model line
|
|
203
203
|
const mainModelName = mainModelMeta?.name || mainModelId;
|
|
204
|
-
|
|
205
|
-
|
|
204
|
+
|
|
205
|
+
// Use "Model" label when only one model, "Main model" when multiple
|
|
206
|
+
const modelLabel = supportingEntries.length > 0 ? 'Main model' : 'Model';
|
|
206
207
|
|
|
207
208
|
if (mainMatches) {
|
|
208
|
-
info += `\n-
|
|
209
|
+
info += `\n- **${modelLabel}: ${mainModelName}** (\`${mainModelId}\`)`;
|
|
209
210
|
} else {
|
|
210
211
|
// Main model doesn't match requested - show warning
|
|
211
|
-
info += `\n-
|
|
212
|
+
info += `\n- **${modelLabel}: ${mainModelName}** (\`${mainModelId}\`)`;
|
|
212
213
|
if (hasRequested) {
|
|
213
214
|
info += `\n- ā ļø **Warning**: Main model \`${mainModelId}\` does not match requested model \`${requestedModel}\``;
|
|
214
215
|
}
|
|
215
216
|
}
|
|
216
217
|
|
|
217
|
-
// Display
|
|
218
|
+
// Display additional models
|
|
218
219
|
if (supportingEntries.length > 0) {
|
|
219
|
-
info += '\n-
|
|
220
|
+
info += '\n- **Additional models:**';
|
|
220
221
|
for (const entry of supportingEntries) {
|
|
221
222
|
const name = entry.modelInfo?.name || entry.modelId;
|
|
222
|
-
|
|
223
|
-
info += `\n - ${name} (\`${entry.modelId}\`${provider ? `, ${provider}` : ''})`;
|
|
223
|
+
info += `\n * **${name}** (\`${entry.modelId}\`)`;
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
} else if (hasModelInfo) {
|