@link-assistant/hive-mind 1.78.10 → 1.78.11
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 +39 -0
- package/package.json +1 -1
- package/src/claude.lib.mjs +11 -2
- package/src/tool-retry.lib.mjs +16 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.78.11
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 24fb17e: fix(retry): auto-resume on server-side 429 "Server is temporarily limiting requests" rate-limit errors (#1924)
|
|
8
|
+
|
|
9
|
+
A long-running solve session (177 turns, ~72 min) was thrown away when the Claude
|
|
10
|
+
CLI surfaced a **server-side temporary rate limit**:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
API Error: Server is temporarily limiting requests (not your usage limit) · Rate limited
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The CLI reports this as a `result` event with `is_error: true` and
|
|
17
|
+
`api_error_status: 429`, and the HTTP response carries `x-should-retry: true`.
|
|
18
|
+
This is a transient throttle that clears on its own — distinct from an account
|
|
19
|
+
usage/quota limit (the message literally says "not your usage limit", and there
|
|
20
|
+
is no reset time to wait for).
|
|
21
|
+
|
|
22
|
+
Root cause: the error matched neither `classifyRetryableError` (no pattern for
|
|
23
|
+
the 429 throttle wording) nor `isUsageLimitError` (correctly, since it is not a
|
|
24
|
+
quota limit), so it fell through to a hard failure with exit code 1 and **no
|
|
25
|
+
auto-resume**, unlike every other transient class (overload 500/529, 503,
|
|
26
|
+
internal server error, request timeout, socket drops).
|
|
27
|
+
|
|
28
|
+
Fix: `classifyRetryableError` (in `src/tool-retry.lib.mjs`, the shared classifier
|
|
29
|
+
used by every tool wrapper — claude, codex, gemini, opencode, qwen, agent) now
|
|
30
|
+
recognises this throttle and marks it retryable (`isCapacity: false`, so no model
|
|
31
|
+
switch), so it retries with the session preserved (`--resume`) after a backoff.
|
|
32
|
+
`src/claude.lib.mjs` additionally detects the structured `api_error_status === 429`
|
|
33
|
+
directly (robust to wording changes) and logs a verbose diagnostic with the
|
|
34
|
+
`request_id`. The matcher is narrow so genuine account usage limits stay on the
|
|
35
|
+
usage-limit reset-time path.
|
|
36
|
+
|
|
37
|
+
Added `tests/test-issue-1924-rate-limit-retry.mjs` (18 assertions) and a full
|
|
38
|
+
case study with timeline, root-cause analysis, upstream references
|
|
39
|
+
(anthropics/claude-code#53915, #53922), and the captured logs under
|
|
40
|
+
`docs/case-studies/issue-1924`.
|
|
41
|
+
|
|
3
42
|
## 1.78.10
|
|
4
43
|
|
|
5
44
|
### Patch Changes
|
package/package.json
CHANGED
package/src/claude.lib.mjs
CHANGED
|
@@ -648,6 +648,7 @@ export const executeClaudeCommand = async params => {
|
|
|
648
648
|
let is503Error = false;
|
|
649
649
|
let isInternalServerError = false;
|
|
650
650
|
let isRequestTimeout = false;
|
|
651
|
+
let isRateLimitError = false; // Issue #1924: server-side 429 temporary rate limiting
|
|
651
652
|
let apiMarkedNotRetryable = false;
|
|
652
653
|
let resultNumTurns = 0;
|
|
653
654
|
let stderrErrors = [];
|
|
@@ -977,6 +978,14 @@ export const executeClaudeCommand = async params => {
|
|
|
977
978
|
isRequestTimeout = true;
|
|
978
979
|
await log('⏱️ Detected request timeout from Claude CLI (will retry with --resume)', { verbose: true });
|
|
979
980
|
}
|
|
981
|
+
// Issue #1924: Server-side temporary rate limiting (HTTP 429) — a transient
|
|
982
|
+
// throttle, not an account usage limit ("...not your usage limit..."), so retry
|
|
983
|
+
// with --resume. The message text is handled by classifyRetryableError; this also
|
|
984
|
+
// catches the structured api_error_status if the wording ever changes.
|
|
985
|
+
if (data.api_error_status === 429) {
|
|
986
|
+
isRateLimitError = true;
|
|
987
|
+
await log(`⚠️ Detected server-side rate limiting (429) from Claude CLI (will retry with --resume). request_id=${data.request_id || 'unknown'}`, { verbose: true });
|
|
988
|
+
}
|
|
980
989
|
// Issue #1834: Detect corrupted extended-thinking-block 400 (un-resumable session).
|
|
981
990
|
// Capture diagnostics (request id, content path) to aid debugging and upstream reports.
|
|
982
991
|
if ((lastMessage.includes('thinking') || lastMessage.includes('redacted_thinking')) && lastMessage.includes('cannot be modified')) {
|
|
@@ -1174,7 +1183,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1174
1183
|
return await executeWithRetry();
|
|
1175
1184
|
}
|
|
1176
1185
|
// Issues #1331, #1353, #1472/#1475: Unified transient error retry (exponential backoff, session preservation)
|
|
1177
|
-
const isTransientError = isStartupTimeout || isActivityTimeout || isOverloadError || isInternalServerError || is503Error || isRequestTimeout || retryableLastError.isRetryable || (lastMessage.includes('API Error: 500') && (lastMessage.includes('Overloaded') || lastMessage.includes('Internal server error'))) || (lastMessage.includes('API Error: 529') && (lastMessage.includes('overloaded_error') || lastMessage.includes('Overloaded'))) || (lastMessage.includes('api_error') && lastMessage.includes('Overloaded')) || (lastMessage.includes('overloaded_error') && lastMessage.includes('Overloaded')) || lastMessage.includes('API Error: 503') || (lastMessage.includes('503') && (lastMessage.includes('upstream connect error') || lastMessage.includes('remote connection failure'))) || lastMessage === 'Request timed out' || lastMessage.includes('Request timed out');
|
|
1186
|
+
const isTransientError = isStartupTimeout || isActivityTimeout || isOverloadError || isInternalServerError || is503Error || isRequestTimeout || isRateLimitError || retryableLastError.isRetryable || (lastMessage.includes('API Error: 500') && (lastMessage.includes('Overloaded') || lastMessage.includes('Internal server error'))) || (lastMessage.includes('API Error: 529') && (lastMessage.includes('overloaded_error') || lastMessage.includes('Overloaded'))) || (lastMessage.includes('api_error') && lastMessage.includes('Overloaded')) || (lastMessage.includes('overloaded_error') && lastMessage.includes('Overloaded')) || lastMessage.includes('API Error: 503') || (lastMessage.includes('503') && (lastMessage.includes('upstream connect error') || lastMessage.includes('remote connection failure'))) || lastMessage === 'Request timed out' || lastMessage.includes('Request timed out');
|
|
1178
1187
|
if ((commandFailed || isTransientError) && isTransientError) {
|
|
1179
1188
|
// Issue #1472/#1475: Startup/activity timeout → 30s–2min backoff; #1353: Request timeout → 5min–1hr; general → 2min–30min
|
|
1180
1189
|
const isTimeoutRetry = isStartupTimeout || isActivityTimeout;
|
|
@@ -1208,7 +1217,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1208
1217
|
}
|
|
1209
1218
|
if (retryCount < maxRetries) {
|
|
1210
1219
|
const delay = Math.min(initialDelay * Math.pow(retryLimits.retryBackoffMultiplier, retryCount), maxDelay);
|
|
1211
|
-
const errorLabel = isStartupTimeout ? 'Stream startup timeout (Issue #1472/#1475)' : isActivityTimeout ? 'Stream activity timeout (Issue #1472)' : isRequestTimeout ? 'Request timeout' : retryableLastError.label || (isOverloadError || (lastMessage.includes('API Error: 500') && lastMessage.includes('Overloaded')) || (lastMessage.includes('API Error: 529') && lastMessage.includes('Overloaded')) ? `API overload (${lastMessage.includes('529') ? '529' : '500'})` : isInternalServerError || lastMessage.includes('Internal server error') ? 'Internal server error (500)' : '503 network error');
|
|
1220
|
+
const errorLabel = isStartupTimeout ? 'Stream startup timeout (Issue #1472/#1475)' : isActivityTimeout ? 'Stream activity timeout (Issue #1472)' : isRequestTimeout ? 'Request timeout' : retryableLastError.label || (isOverloadError || (lastMessage.includes('API Error: 500') && lastMessage.includes('Overloaded')) || (lastMessage.includes('API Error: 529') && lastMessage.includes('Overloaded')) ? `API overload (${lastMessage.includes('529') ? '529' : '500'})` : isInternalServerError || lastMessage.includes('Internal server error') ? 'Internal server error (500)' : isRateLimitError ? 'Server rate limited (429)' : '503 network error');
|
|
1212
1221
|
const notRetryableHint = apiMarkedNotRetryable ? ' (API says not retryable — will stop early if no progress)' : '';
|
|
1213
1222
|
const delayLabel = delay >= 60000 ? `${Math.round(delay / 60000)} min` : `${Math.round(delay / 1000)}s`;
|
|
1214
1223
|
const retryMode = isStartupTimeout ? ' (fresh start)' : ' (session preserved)';
|
package/src/tool-retry.lib.mjs
CHANGED
|
@@ -72,6 +72,22 @@ export const classifyRetryableError = value => {
|
|
|
72
72
|
return { message, isRetryable: false, isCapacity: false, requiresFreshSession: true, label: 'Corrupted thinking blocks (un-resumable session)' };
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// Issue #1924: Server-side temporary rate limiting (HTTP 429), distinct from an
|
|
76
|
+
// account usage/quota limit. The Claude CLI surfaces this as a synthetic
|
|
77
|
+
// assistant/result message and an api_error_status of 429:
|
|
78
|
+
// "API Error: Server is temporarily limiting requests (not your usage limit) · Rate limited"
|
|
79
|
+
// The response carries `x-should-retry: true` and the stream emits a
|
|
80
|
+
// `rate_limit_event` with `status: "rejected"`. Because the message explicitly
|
|
81
|
+
// says "not your usage limit", it is NOT a usage-limit reset-time situation and
|
|
82
|
+
// must NOT be routed through detectUsageLimit() (there is no reset time to wait
|
|
83
|
+
// for). It is a transient throttle that clears on its own, so it is safe to
|
|
84
|
+
// retry with the session preserved (--resume) after a backoff. Switching models
|
|
85
|
+
// does not help (the throttle is request-rate, not model capacity), so
|
|
86
|
+
// isCapacity is false.
|
|
87
|
+
if (lower.includes('temporarily limiting requests') || (lower.includes('rate limited') && lower.includes('not your usage limit')) || (lower.includes('rate_limit') && lower.includes('429'))) {
|
|
88
|
+
return { message, isRetryable: true, isCapacity: false, label: 'Server rate limited (429)' };
|
|
89
|
+
}
|
|
90
|
+
|
|
75
91
|
if (lower.includes('api error: 503') || (lower.includes('503') && (lower.includes('upstream connect error') || lower.includes('remote connection failure')))) {
|
|
76
92
|
return { message, isRetryable: true, isCapacity: false, label: '503 network error' };
|
|
77
93
|
}
|