@link-assistant/hive-mind 2.12.5 → 2.13.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 +12 -0
- package/package.json +1 -1
- package/src/claude.budget-stats.lib.mjs +4 -2
- package/src/claude.lib.mjs +83 -17
- package/src/claude.stream-events.lib.mjs +56 -2
- package/src/disk-guard.lib.mjs +256 -0
- package/src/github.batch.lib.mjs +31 -11
- package/src/github.lib.mjs +3 -1
- package/src/hive.mjs +136 -11
- package/src/lib.mjs +23 -3
- package/src/limits-i18n.lib.mjs +8 -0
- package/src/list-solution-drafts.lib.mjs +17 -4
- package/src/locales/en.lino +10 -0
- package/src/locales/hi.lino +10 -0
- package/src/locales/ru.lino +10 -0
- package/src/locales/zh.lino +10 -0
- package/src/session-log-rename.lib.mjs +65 -0
- package/src/session-monitor.lib.mjs +45 -1
- package/src/solve.mjs +48 -4
- package/src/solve.repository.lib.mjs +5 -1
- package/src/solve.restart-shared.lib.mjs +18 -9
- package/src/solve.results.lib.mjs +6 -3
- package/src/solve.validation.lib.mjs +7 -9
- package/src/subscription-block-telegram.lib.mjs +115 -0
- package/src/subscription-error.lib.mjs +328 -0
- package/src/tool-retry.lib.mjs +29 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription / account-access error detection for AI CLI tools.
|
|
3
|
+
*
|
|
4
|
+
* Issue #2161: a `/solve` run died after 4h11m and $31.39 of work with nothing
|
|
5
|
+
* but the generic line
|
|
6
|
+
*
|
|
7
|
+
* ❌ CLAUDE execution failed with Your organization has disabled Claude
|
|
8
|
+
* subscription access for Claude Code · Use an Anthropic API key instead,
|
|
9
|
+
* or ask your admin to enable access
|
|
10
|
+
*
|
|
11
|
+
* That sentence is not a transient API fault and not a usage limit: it means the
|
|
12
|
+
* *account itself* is no longer permitted to use the tool. Waiting does not help,
|
|
13
|
+
* retrying does not help, switching to a fallback model does not help — the run
|
|
14
|
+
* must stop immediately, preserve the work, and tell the operator exactly what to
|
|
15
|
+
* do.
|
|
16
|
+
*
|
|
17
|
+
* This module is the single place that recognises that whole class of errors for
|
|
18
|
+
* every tool hive-mind can drive. Two detection layers are used, strongest first:
|
|
19
|
+
*
|
|
20
|
+
* 1. Machine-readable codes emitted by the tool (Claude Code's `error` field on
|
|
21
|
+
* stream-json `assistant`/`result` events, Codex's auth error codes). These
|
|
22
|
+
* are exact and locale independent.
|
|
23
|
+
* 2. Verbatim user-facing strings, transcribed from the shipped CLI binaries
|
|
24
|
+
* (see docs/case-studies/issue-2161/provider-error-strings.md). Used when
|
|
25
|
+
* only the rendered message survives (most tools give us nothing else).
|
|
26
|
+
*
|
|
27
|
+
* Deliberately NOT matched here:
|
|
28
|
+
* - "Authentication error · This may be a temporary network issue, please try
|
|
29
|
+
* again" — Claude Code's own wording says it is transient, so it belongs to
|
|
30
|
+
* the retry path, not to this terminal path.
|
|
31
|
+
* - Usage/quota limits ("You've hit your usage limit", "resets 5am") — those
|
|
32
|
+
* have a reset time and are handled by usage-limit.lib.mjs.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** Emitted verbatim into the log so /hive, the Telegram monitor and humans can grep for it. */
|
|
36
|
+
export const SUBSCRIPTION_BLOCKED_MARKER = '🚫 SUBSCRIPTION/ACCESS BLOCKED';
|
|
37
|
+
|
|
38
|
+
export const SUBSCRIPTION_ERROR_KINDS = {
|
|
39
|
+
ORG_SUBSCRIPTION_DISABLED: 'org_subscription_disabled',
|
|
40
|
+
ACCOUNT_NO_ACCESS: 'account_no_access',
|
|
41
|
+
LOGIN_REQUIRED: 'login_required',
|
|
42
|
+
BILLING: 'billing',
|
|
43
|
+
PLAN_RESTRICTED: 'plan_restricted',
|
|
44
|
+
API_KEY_INVALID: 'api_key_invalid',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const K = SUBSCRIPTION_ERROR_KINDS;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Machine-readable codes → kind. Sources:
|
|
51
|
+
* - Claude Code CLI 2.1.233, blocked-state switch (`oauth_org_not_allowed`,
|
|
52
|
+
* `authentication_failed`, `billing_error`, …).
|
|
53
|
+
* - Codex CLI 0.147.0 auth error codes (`missing_codex_entitlement`,
|
|
54
|
+
* `refresh_token_expired`, `disabled_by_admin`, `plan_not_eligible`, …).
|
|
55
|
+
*/
|
|
56
|
+
export const SUBSCRIPTION_ERROR_CODES = Object.freeze({
|
|
57
|
+
// Claude Code
|
|
58
|
+
oauth_org_not_allowed: K.ORG_SUBSCRIPTION_DISABLED,
|
|
59
|
+
authentication_failed: K.LOGIN_REQUIRED,
|
|
60
|
+
token_revoked: K.LOGIN_REQUIRED,
|
|
61
|
+
invalid_api_key: K.API_KEY_INVALID,
|
|
62
|
+
billing_error: K.BILLING,
|
|
63
|
+
credit_balance_low: K.BILLING,
|
|
64
|
+
// Codex
|
|
65
|
+
missing_codex_entitlement: K.ACCOUNT_NO_ACCESS,
|
|
66
|
+
disabled_by_admin: K.ORG_SUBSCRIPTION_DISABLED,
|
|
67
|
+
plan_not_eligible: K.PLAN_RESTRICTED,
|
|
68
|
+
required_app_unavailable: K.ACCOUNT_NO_ACCESS,
|
|
69
|
+
refresh_token_expired: K.LOGIN_REQUIRED,
|
|
70
|
+
refresh_token_invalidated: K.LOGIN_REQUIRED,
|
|
71
|
+
not_chatgpt_auth: K.LOGIN_REQUIRED,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Substrings that look authentication-ish but are explicitly transient. Checked
|
|
76
|
+
* before every other rule so a network blip is never reported as a cancelled
|
|
77
|
+
* subscription (which would stop the whole hive).
|
|
78
|
+
*/
|
|
79
|
+
const TRANSIENT_AUTH_PATTERNS = ['this may be a temporary network issue', 'could not authenticate with its upstream provider', 'temporary failure in name resolution'];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Verbatim strings from the shipped CLIs, lower-cased. `tool` is informational:
|
|
83
|
+
* a message is matched regardless of which tool produced it, because hive-mind
|
|
84
|
+
* often only sees the rendered text several layers away from its origin.
|
|
85
|
+
*/
|
|
86
|
+
const MESSAGE_RULES = [
|
|
87
|
+
// ---- Claude Code -------------------------------------------------------
|
|
88
|
+
{ kind: K.ORG_SUBSCRIPTION_DISABLED, tool: 'claude', needles: ['organization has disabled claude subscription access'] },
|
|
89
|
+
{ kind: K.ORG_SUBSCRIPTION_DISABLED, tool: 'claude', needles: ['organization has disabled api key authentication'] },
|
|
90
|
+
{ kind: K.ORG_SUBSCRIPTION_DISABLED, tool: 'claude', needles: ['belongs to a disabled organization'] },
|
|
91
|
+
{ kind: K.ORG_SUBSCRIPTION_DISABLED, tool: 'claude', needles: ['organization has been disabled'] },
|
|
92
|
+
{ kind: K.ACCOUNT_NO_ACCESS, tool: 'claude', needles: ['your account does not have access to claude'] },
|
|
93
|
+
{ kind: K.LOGIN_REQUIRED, tool: 'claude', needles: ['oauth token revoked'] },
|
|
94
|
+
{ kind: K.LOGIN_REQUIRED, tool: 'claude', needles: ['login expired'] },
|
|
95
|
+
{ kind: K.LOGIN_REQUIRED, tool: 'claude', needles: ['not logged in'] },
|
|
96
|
+
{ kind: K.LOGIN_REQUIRED, tool: 'claude', needles: ['session expired. please run /login'] },
|
|
97
|
+
{ kind: K.LOGIN_REQUIRED, tool: 'claude', needles: ['oauth session expired and could not be refreshed'] },
|
|
98
|
+
{ kind: K.LOGIN_REQUIRED, tool: 'claude', needles: ['anthropic profile login expired'] },
|
|
99
|
+
{ kind: K.BILLING, tool: 'claude', needles: ['credit balance is too low'] },
|
|
100
|
+
{ kind: K.API_KEY_INVALID, tool: 'claude', needles: ['invalid api key'] },
|
|
101
|
+
{ kind: K.API_KEY_INVALID, tool: 'claude', needles: ['invalid auth token'] },
|
|
102
|
+
{ kind: K.PLAN_RESTRICTED, tool: 'claude', needles: ['is not available with the claude pro plan'] },
|
|
103
|
+
{ kind: K.PLAN_RESTRICTED, tool: 'claude', needles: ['auto mode is unavailable for your plan'] },
|
|
104
|
+
|
|
105
|
+
// ---- Codex -------------------------------------------------------------
|
|
106
|
+
{ kind: K.ACCOUNT_NO_ACCESS, tool: 'codex', needles: ['you do not have access to codex'] },
|
|
107
|
+
{ kind: K.ACCOUNT_NO_ACCESS, tool: 'codex', needles: ['not currently authorized to use codex'] },
|
|
108
|
+
{ kind: K.ACCOUNT_NO_ACCESS, tool: 'codex', needles: ['contact your workspace administrator to request access to codex'] },
|
|
109
|
+
{ kind: K.LOGIN_REQUIRED, tool: 'codex', needles: ['access token could not be refreshed'] },
|
|
110
|
+
{ kind: K.LOGIN_REQUIRED, tool: 'codex', needles: ['oauth refresh token was rejected'] },
|
|
111
|
+
{ kind: K.LOGIN_REQUIRED, tool: 'codex', needles: ["not signed in. please run 'codex login'"] },
|
|
112
|
+
|
|
113
|
+
// ---- Qwen Code ---------------------------------------------------------
|
|
114
|
+
{ kind: K.LOGIN_REQUIRED, tool: 'qwen', needles: ['qwen oauth credentials expired'] },
|
|
115
|
+
{ kind: K.LOGIN_REQUIRED, tool: 'qwen', needles: ['refresh token expired or invalid'] },
|
|
116
|
+
{ kind: K.LOGIN_REQUIRED, tool: 'qwen', needles: ['failed to obtain valid qwen access token'] },
|
|
117
|
+
{ kind: K.PLAN_RESTRICTED, tool: 'qwen', needles: ['coding plan api key not found'] },
|
|
118
|
+
|
|
119
|
+
// ---- Gemini CLI --------------------------------------------------------
|
|
120
|
+
{ kind: K.LOGIN_REQUIRED, tool: 'gemini', needles: ['please re-authenticate with the correct type'] },
|
|
121
|
+
{ kind: K.PLAN_RESTRICTED, tool: 'gemini', needles: ["doesn't have a gemini code assist"] },
|
|
122
|
+
|
|
123
|
+
// ---- OpenCode ----------------------------------------------------------
|
|
124
|
+
{ kind: K.LOGIN_REQUIRED, tool: 'opencode', needles: ['run `opencode auth login` in the terminal'] },
|
|
125
|
+
{ kind: K.LOGIN_REQUIRED, tool: 'opencode', needles: ['oauth token refresh failed and no fallback'] },
|
|
126
|
+
{ kind: K.ACCOUNT_NO_ACCESS, tool: 'opencode', needles: ['your account does not have access to ai features'] },
|
|
127
|
+
|
|
128
|
+
// ---- Generic provider phrasing (any tool) ------------------------------
|
|
129
|
+
{ kind: K.ORG_SUBSCRIPTION_DISABLED, tool: null, needles: ['has disabled', 'subscription access'] },
|
|
130
|
+
{ kind: K.BILLING, tool: null, needles: ['subscription', 'expired'] },
|
|
131
|
+
{ kind: K.BILLING, tool: null, needles: ['subscription', 'cancel'] },
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
const KIND_LABELS = Object.freeze({
|
|
135
|
+
[K.ORG_SUBSCRIPTION_DISABLED]: 'Subscription access disabled for this organization',
|
|
136
|
+
[K.ACCOUNT_NO_ACCESS]: 'Account is not authorized to use this tool',
|
|
137
|
+
[K.LOGIN_REQUIRED]: 'Authentication expired — re-login required',
|
|
138
|
+
[K.BILLING]: 'Subscription/billing problem',
|
|
139
|
+
[K.PLAN_RESTRICTED]: 'Current plan does not allow this request',
|
|
140
|
+
[K.API_KEY_INVALID]: 'Invalid API key or auth token',
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const KIND_REASONS = Object.freeze({
|
|
144
|
+
[K.ORG_SUBSCRIPTION_DISABLED]: 'The provider rejected the request because the organization/account behind the subscription is no longer allowed to use this CLI. This is an account-level block, not a rate limit — it will not clear on its own.',
|
|
145
|
+
[K.ACCOUNT_NO_ACCESS]: 'The provider accepted the credentials but the account has no entitlement for this product. Access must be granted before any further run can succeed.',
|
|
146
|
+
[K.LOGIN_REQUIRED]: 'The stored OAuth credentials are gone, revoked or unrefreshable. Every request will fail until the tool is logged in again.',
|
|
147
|
+
[K.BILLING]: 'The subscription is expired, cancelled or out of credit. Requests stay rejected until billing is restored.',
|
|
148
|
+
[K.PLAN_RESTRICTED]: 'The account is authenticated, but the requested model/mode is not included in the current plan.',
|
|
149
|
+
[K.API_KEY_INVALID]: 'The configured API key or auth token was rejected by the provider.',
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
/** Per-tool re-authentication commands, used to build actionable guidance. */
|
|
153
|
+
const TOOL_LOGIN_HINTS = Object.freeze({
|
|
154
|
+
claude: 'claude /login (or set ANTHROPIC_API_KEY for API-key billing)',
|
|
155
|
+
codex: 'codex login (add --device-auth on a headless machine)',
|
|
156
|
+
qwen: 'qwen → /auth',
|
|
157
|
+
gemini: 'gemini → /auth',
|
|
158
|
+
opencode: 'opencode auth login',
|
|
159
|
+
agent: 'agent auth login',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const TOOL_ACCOUNT_URLS = Object.freeze({
|
|
163
|
+
claude: 'https://claude.ai/settings/billing',
|
|
164
|
+
codex: 'https://chatgpt.com/codex/settings/usage',
|
|
165
|
+
qwen: 'https://chat.qwen.ai',
|
|
166
|
+
gemini: 'https://codeassist.google.com',
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const toText = value => {
|
|
170
|
+
if (value === null || value === undefined) return '';
|
|
171
|
+
if (typeof value === 'string') return value;
|
|
172
|
+
if (typeof value?.error?.message === 'string') return value.error.message;
|
|
173
|
+
if (typeof value?.message === 'string') return value.message;
|
|
174
|
+
try {
|
|
175
|
+
return JSON.stringify(value);
|
|
176
|
+
} catch {
|
|
177
|
+
return String(value);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* True when the text is an authentication-flavoured error that the provider
|
|
183
|
+
* itself describes as temporary. Such errors must keep using the retry path.
|
|
184
|
+
*/
|
|
185
|
+
export const isTransientAuthError = value => {
|
|
186
|
+
const lower = toText(value).toLowerCase();
|
|
187
|
+
if (!lower) return false;
|
|
188
|
+
return TRANSIENT_AUTH_PATTERNS.some(p => lower.includes(p));
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const buildGuidance = (kind, tool) => {
|
|
192
|
+
const loginHint = TOOL_LOGIN_HINTS[tool] || TOOL_LOGIN_HINTS.claude;
|
|
193
|
+
const accountUrl = TOOL_ACCOUNT_URLS[tool] || null;
|
|
194
|
+
const steps = [];
|
|
195
|
+
switch (kind) {
|
|
196
|
+
case K.ORG_SUBSCRIPTION_DISABLED:
|
|
197
|
+
steps.push('Ask the organization/workspace admin to re-enable CLI access for this account.');
|
|
198
|
+
steps.push('Or switch this run to API-key billing instead of the subscription.');
|
|
199
|
+
break;
|
|
200
|
+
case K.ACCOUNT_NO_ACCESS:
|
|
201
|
+
steps.push('Request access for this account from the workspace administrator.');
|
|
202
|
+
steps.push('Verify you are logged in with the account that actually owns the subscription.');
|
|
203
|
+
break;
|
|
204
|
+
case K.LOGIN_REQUIRED:
|
|
205
|
+
steps.push(`Re-authenticate the tool: ${loginHint}`);
|
|
206
|
+
break;
|
|
207
|
+
case K.BILLING:
|
|
208
|
+
steps.push('Renew/reactivate the subscription or top up the credit balance.');
|
|
209
|
+
if (accountUrl) steps.push(`Billing page: ${accountUrl}`);
|
|
210
|
+
break;
|
|
211
|
+
case K.PLAN_RESTRICTED:
|
|
212
|
+
steps.push('Pick a model/mode included in the current plan (see --model), or upgrade the plan.');
|
|
213
|
+
steps.push(`After a plan change, re-login so the new entitlements are picked up: ${loginHint}`);
|
|
214
|
+
break;
|
|
215
|
+
case K.API_KEY_INVALID:
|
|
216
|
+
steps.push('Fix or regenerate the configured API key / auth token, then re-run.');
|
|
217
|
+
break;
|
|
218
|
+
default:
|
|
219
|
+
steps.push(`Re-authenticate the tool: ${loginHint}`);
|
|
220
|
+
}
|
|
221
|
+
steps.push('Once access is restored, resume with the session ID printed above — no work is lost.');
|
|
222
|
+
return steps;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Detect an account/subscription-level block.
|
|
227
|
+
*
|
|
228
|
+
* @param {string|Object} input - Raw message, or a descriptor:
|
|
229
|
+
* { message, tool, errorCode, apiErrorStatus, terminalReason }
|
|
230
|
+
* @returns {null|{isSubscriptionError: true, kind, code, tool, label, reason, message, guidance, apiErrorStatus}}
|
|
231
|
+
*/
|
|
232
|
+
export const detectSubscriptionError = input => {
|
|
233
|
+
const descriptor = typeof input === 'string' || input === null || input === undefined ? { message: input } : input;
|
|
234
|
+
const message = toText(descriptor.message ?? descriptor);
|
|
235
|
+
const tool = descriptor.tool ? String(descriptor.tool).toLowerCase() : null;
|
|
236
|
+
const rawCode = descriptor.errorCode ? String(descriptor.errorCode).toLowerCase().trim() : null;
|
|
237
|
+
const apiErrorStatus = Number.isFinite(descriptor.apiErrorStatus) ? descriptor.apiErrorStatus : null;
|
|
238
|
+
|
|
239
|
+
// Layer 1: machine-readable code. Trusted even when the message is missing.
|
|
240
|
+
if (rawCode && Object.hasOwn(SUBSCRIPTION_ERROR_CODES, rawCode)) {
|
|
241
|
+
const kind = SUBSCRIPTION_ERROR_CODES[rawCode];
|
|
242
|
+
return {
|
|
243
|
+
isSubscriptionError: true,
|
|
244
|
+
kind,
|
|
245
|
+
code: rawCode,
|
|
246
|
+
tool,
|
|
247
|
+
label: KIND_LABELS[kind],
|
|
248
|
+
reason: KIND_REASONS[kind],
|
|
249
|
+
message,
|
|
250
|
+
apiErrorStatus,
|
|
251
|
+
guidance: buildGuidance(kind, tool),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!message) return null;
|
|
256
|
+
const lower = message.toLowerCase();
|
|
257
|
+
if (isTransientAuthError(lower)) return null;
|
|
258
|
+
|
|
259
|
+
// Layer 2: verbatim provider strings.
|
|
260
|
+
for (const rule of MESSAGE_RULES) {
|
|
261
|
+
if (!rule.needles.every(n => lower.includes(n))) continue;
|
|
262
|
+
return {
|
|
263
|
+
isSubscriptionError: true,
|
|
264
|
+
kind: rule.kind,
|
|
265
|
+
code: rawCode || null,
|
|
266
|
+
tool: tool || rule.tool || null,
|
|
267
|
+
label: KIND_LABELS[rule.kind],
|
|
268
|
+
reason: KIND_REASONS[rule.kind],
|
|
269
|
+
message,
|
|
270
|
+
apiErrorStatus,
|
|
271
|
+
guidance: buildGuidance(rule.kind, tool || rule.tool),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/** Convenience boolean wrapper mirroring isUsageLimitError(). */
|
|
278
|
+
export const isSubscriptionBlockedError = input => detectSubscriptionError(input) !== null;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Render the terminal/log block. The first line is SUBSCRIPTION_BLOCKED_MARKER so
|
|
282
|
+
* downstream consumers (/hive worker output scanner, Telegram session monitor,
|
|
283
|
+
* `grep`) have one stable anchor.
|
|
284
|
+
*
|
|
285
|
+
* @returns {string[]} lines
|
|
286
|
+
*/
|
|
287
|
+
export const formatSubscriptionErrorReport = (info, { tool = null, sessionId = null, tempDir = null, branchName = null, committed = null, resumeCommand = null } = {}) => {
|
|
288
|
+
if (!info) return [];
|
|
289
|
+
const toolName = (info.tool || tool || 'tool').toUpperCase();
|
|
290
|
+
const lines = [];
|
|
291
|
+
lines.push('');
|
|
292
|
+
lines.push(`${SUBSCRIPTION_BLOCKED_MARKER} — ${toolName}: ${info.label}`);
|
|
293
|
+
lines.push(` Provider said: ${info.message || '(no message)'}`);
|
|
294
|
+
if (info.code) lines.push(` Error code: ${info.code}${info.apiErrorStatus ? ` (HTTP ${info.apiErrorStatus})` : ''}`);
|
|
295
|
+
else if (info.apiErrorStatus) lines.push(` HTTP status: ${info.apiErrorStatus}`);
|
|
296
|
+
lines.push(` Why this stops the run: ${info.reason}`);
|
|
297
|
+
lines.push(' This is NOT a usage limit and NOT a transient API error — retrying, waiting for a reset');
|
|
298
|
+
lines.push(' or switching to a fallback model cannot fix it, so the task is stopped now.');
|
|
299
|
+
lines.push('');
|
|
300
|
+
lines.push(' What to do:');
|
|
301
|
+
for (const step of info.guidance || []) lines.push(` • ${step}`);
|
|
302
|
+
if (committed === true) lines.push(' 💾 Uncommitted changes were auto-committed and pushed before stopping.');
|
|
303
|
+
else if (committed === false) lines.push(' ⚠️ No uncommitted changes to preserve (working tree was clean).');
|
|
304
|
+
if (tempDir) lines.push(` 📁 Working directory: ${tempDir}`);
|
|
305
|
+
if (branchName) lines.push(` 🌿 Branch: ${branchName}`);
|
|
306
|
+
if (sessionId) lines.push(` 📌 Session ID: ${sessionId}`);
|
|
307
|
+
if (resumeCommand) lines.push(` ▶️ Resume after access is restored: ${resumeCommand}`);
|
|
308
|
+
lines.push('');
|
|
309
|
+
return lines;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
/** One-line summary used for exit messages, PR comments and commit reasons. */
|
|
313
|
+
export const formatSubscriptionErrorSummary = (info, { tool = null } = {}) => {
|
|
314
|
+
if (!info) return '';
|
|
315
|
+
const toolName = (info.tool || tool || 'tool').toUpperCase();
|
|
316
|
+
return `${toolName} stopped: ${info.label}${info.code ? ` [${info.code}]` : ''} — ${info.message || ''}`.trim();
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
export default {
|
|
320
|
+
SUBSCRIPTION_BLOCKED_MARKER,
|
|
321
|
+
SUBSCRIPTION_ERROR_KINDS,
|
|
322
|
+
SUBSCRIPTION_ERROR_CODES,
|
|
323
|
+
detectSubscriptionError,
|
|
324
|
+
isSubscriptionBlockedError,
|
|
325
|
+
isTransientAuthError,
|
|
326
|
+
formatSubscriptionErrorReport,
|
|
327
|
+
formatSubscriptionErrorSummary,
|
|
328
|
+
};
|
package/src/tool-retry.lib.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { retryLimits } from './config.lib.mjs';
|
|
4
4
|
import { resolveDefaultFallbackModel, resolveModelId } from './models/index.mjs';
|
|
5
|
+
import { detectSubscriptionError, isTransientAuthError } from './subscription-error.lib.mjs';
|
|
5
6
|
|
|
6
7
|
const normalizeMessage = value => {
|
|
7
8
|
if (value === null || value === undefined) return '';
|
|
@@ -27,6 +28,34 @@ export const classifyRetryableError = value => {
|
|
|
27
28
|
const message = normalizeMessage(value);
|
|
28
29
|
const lower = message.toLowerCase();
|
|
29
30
|
|
|
31
|
+
// Issue #2161: account/subscription-level blocks ("Your organization has
|
|
32
|
+
// disabled Claude subscription access for Claude Code", "You do not have
|
|
33
|
+
// access to Codex", revoked OAuth tokens, expired subscriptions, …). These are
|
|
34
|
+
// terminal by nature: the credentials themselves are no longer accepted, so
|
|
35
|
+
// neither a backoff nor a different model can recover — the run must stop and
|
|
36
|
+
// the operator must restore access. isCapacity stays false so
|
|
37
|
+
// maybeSwitchToFallbackModel() never burns a fallback hop on them.
|
|
38
|
+
//
|
|
39
|
+
// Checked first, because several of these messages contain words ("timed
|
|
40
|
+
// out", "rate", "503") that later transient branches would otherwise claim.
|
|
41
|
+
// detectSubscriptionError() itself excludes provider errors that are
|
|
42
|
+
// explicitly described as temporary — see isTransientAuthError below.
|
|
43
|
+
const subscriptionError = detectSubscriptionError(message);
|
|
44
|
+
if (subscriptionError) {
|
|
45
|
+
return { message, isRetryable: false, isCapacity: false, isSubscriptionError: true, subscriptionError, label: 'subscription access blocked' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Issue #2161: the counterpart — Claude Code's own wording marks this
|
|
49
|
+
// authentication failure as temporary ("This may be a temporary network
|
|
50
|
+
// issue, please try again"). Without an explicit branch it would fall through
|
|
51
|
+
// to the non-retryable default and abort a run that a retry would have saved.
|
|
52
|
+
// The `auth` guard keeps purely network-level members of that list (e.g.
|
|
53
|
+
// "Temporary failure in name resolution") on their own, more specific branches
|
|
54
|
+
// below — this branch only claims the *authentication* wordings.
|
|
55
|
+
if (isTransientAuthError(lower) && lower.includes('auth')) {
|
|
56
|
+
return { message, isRetryable: true, isCapacity: false, label: 'Transient authentication/network error' };
|
|
57
|
+
}
|
|
58
|
+
|
|
30
59
|
// Genuine model-specific capacity: the API explicitly tells us this *particular*
|
|
31
60
|
// model is full and recommends trying a *different* model (e.g. Codex's
|
|
32
61
|
// "Selected model is at capacity. Please try a different model."). Here a model
|