@foxden-app/foxclaw 0.5.21 → 0.5.22
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 +10 -0
- package/dist/controller/controller.js +44 -17
- package/dist/i18n.d.ts +10 -0
- package/dist/i18n.js +10 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable FoxClaw changes are listed here. Each release note is bilingual so GitHub Releases and the npm package are useful to both Chinese and English readers.
|
|
4
4
|
|
|
5
|
+
## 0.5.22 - 2026-06-09
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- Codex 报 `usageLimitExceeded`、`You've hit your usage limit`、usage/rate/quota/billing/credits limit 这类额度耗尽时,不再把当前 auth 候选标记为需要修复,也不会触发 `AUTH_AUTO_DELETE_NEEDS_REPAIR` 的自动剔除和跨节点删除。
|
|
9
|
+
- 额度耗尽仍会临时避开当前候选,切到另一个维护中的候选重试;提示文案改为“Codex 额度限制”,不再误写成 “Codex auth 问题”。
|
|
10
|
+
|
|
11
|
+
### English
|
|
12
|
+
- Codex quota exhaustion such as `usageLimitExceeded`, `You've hit your usage limit`, usage/rate/quota/billing/credits limit is no longer treated as an invalid auth candidate, so it does not mark `needs_repair` or trigger `AUTH_AUTO_DELETE_NEEDS_REPAIR` auto-delete / cross-node delete.
|
|
13
|
+
- Quota exhaustion still temporarily skips the current candidate and retries with another maintained candidate; notifications now call this a Codex usage limit instead of an auth problem.
|
|
14
|
+
|
|
5
15
|
## 0.5.21 - 2026-06-09
|
|
6
16
|
|
|
7
17
|
### 中文
|
|
@@ -1390,7 +1390,8 @@ export class BridgeSessionCore {
|
|
|
1390
1390
|
: threadId
|
|
1391
1391
|
? this.findActiveTurnsByThreadId(threadId)
|
|
1392
1392
|
: [];
|
|
1393
|
-
const
|
|
1393
|
+
const authRotationReason = classifyCodexAuthRotationError(params);
|
|
1394
|
+
const isAuthRotationError = authRotationReason !== null;
|
|
1394
1395
|
const willRetry = params?.willRetry === true;
|
|
1395
1396
|
const active = isAuthRotationError
|
|
1396
1397
|
? activeTurns.find(turn => turn.authRetry !== null) ?? activeTurns.find(turn => !turn.isObserved) ?? activeTurns[0] ?? null
|
|
@@ -1403,12 +1404,13 @@ export class BridgeSessionCore {
|
|
|
1403
1404
|
else if (turnId) {
|
|
1404
1405
|
this.pendingTurnErrors.set(turnId, message);
|
|
1405
1406
|
}
|
|
1406
|
-
if (
|
|
1407
|
+
if (authRotationReason && !willRetry && active?.authRetry) {
|
|
1407
1408
|
const scopeId = active.scopeId;
|
|
1408
1409
|
if (scopeId) {
|
|
1409
1410
|
this.pendingAuthRotation = {
|
|
1410
1411
|
scopeId,
|
|
1411
1412
|
reason: message,
|
|
1413
|
+
reasonKind: authRotationReason,
|
|
1412
1414
|
retry: cloneAuthRetryContext(active.authRetry),
|
|
1413
1415
|
};
|
|
1414
1416
|
}
|
|
@@ -5504,7 +5506,7 @@ export class BridgeSessionCore {
|
|
|
5504
5506
|
const failedTargets = rotation.retry?.failedAuthTargets ?? this.authRotationFailedTargets;
|
|
5505
5507
|
const locale = this.localeForChat(rotation.scopeId);
|
|
5506
5508
|
const current = (await this.listCodexAuthState()).candidates.find(candidate => candidate.isCurrent) ?? null;
|
|
5507
|
-
if (current) {
|
|
5509
|
+
if (current && rotation.reasonKind === 'auth_invalid') {
|
|
5508
5510
|
const recoveredCurrent = await this.recoverCodexAuthCandidate(current.name);
|
|
5509
5511
|
if (recoveredCurrent) {
|
|
5510
5512
|
await this.sendMessage(rotation.scopeId, t(locale, 'auth_auto_recovered_current', {
|
|
@@ -5528,19 +5530,22 @@ export class BridgeSessionCore {
|
|
|
5528
5530
|
}
|
|
5529
5531
|
const selection = await this.selectNextCodexAuthCandidate(failedTargets);
|
|
5530
5532
|
if (!selection) {
|
|
5531
|
-
await this.sendMessage(rotation.scopeId, t(locale, 'auth_auto_no_candidate', {
|
|
5533
|
+
await this.sendMessage(rotation.scopeId, t(locale, rotation.reasonKind === 'quota_limited' ? 'auth_quota_no_candidate' : 'auth_auto_no_candidate', {
|
|
5532
5534
|
error: formatShortStatusError(rotation.reason),
|
|
5533
5535
|
}));
|
|
5534
5536
|
return false;
|
|
5535
5537
|
}
|
|
5536
5538
|
const { candidate, fromLabel, toLabel } = selection;
|
|
5539
|
+
const switchingKey = rotation.reasonKind === 'quota_limited'
|
|
5540
|
+
? (this.config.authAutoDeleteNeedsRepair ? 'auth_quota_switching_quiet' : 'auth_quota_switching')
|
|
5541
|
+
: (this.config.authAutoDeleteNeedsRepair ? 'auth_auto_switching_quiet' : 'auth_auto_switching');
|
|
5537
5542
|
await this.sendMessage(rotation.scopeId, this.config.authAutoDeleteNeedsRepair
|
|
5538
|
-
? t(locale,
|
|
5539
|
-
: t(locale,
|
|
5543
|
+
? t(locale, switchingKey, { error: formatShortStatusError(rotation.reason) })
|
|
5544
|
+
: t(locale, switchingKey, {
|
|
5540
5545
|
...this.codexAuthSwitchParams(locale, fromLabel, toLabel),
|
|
5541
5546
|
error: formatShortStatusError(rotation.reason),
|
|
5542
5547
|
}));
|
|
5543
|
-
const outcome = await this.switchCodexAuthAndRestart(rotation.scopeId, locale, candidate, true);
|
|
5548
|
+
const outcome = await this.switchCodexAuthAndRestart(rotation.scopeId, locale, candidate, true, true, rotation.reasonKind);
|
|
5544
5549
|
if (!outcome.ok) {
|
|
5545
5550
|
return false;
|
|
5546
5551
|
}
|
|
@@ -5659,7 +5664,7 @@ export class BridgeSessionCore {
|
|
|
5659
5664
|
to: toLabel,
|
|
5660
5665
|
};
|
|
5661
5666
|
}
|
|
5662
|
-
async switchCodexAuthAndRestart(scopeId, locale, candidate, automatic, sendResult = true) {
|
|
5667
|
+
async switchCodexAuthAndRestart(scopeId, locale, candidate, automatic, sendResult = true, automaticReason = 'auth_invalid') {
|
|
5663
5668
|
const authDir = this.resolveAuthDir();
|
|
5664
5669
|
const initialState = await listCodexAuthState(new Set(), new Map(), authDir);
|
|
5665
5670
|
const authStat = await fs.lstat(initialState.authPath).catch(() => null);
|
|
@@ -5720,9 +5725,11 @@ export class BridgeSessionCore {
|
|
|
5720
5725
|
if (!sendResult) {
|
|
5721
5726
|
return outcome;
|
|
5722
5727
|
}
|
|
5728
|
+
const doneKey = automaticReason === 'quota_limited' ? 'auth_quota_done' : 'auth_auto_done';
|
|
5729
|
+
const doneQuietKey = automaticReason === 'quota_limited' ? 'auth_quota_done_quiet' : 'auth_auto_done_quiet';
|
|
5723
5730
|
const lines = [automatic && this.config.authAutoDeleteNeedsRepair
|
|
5724
|
-
? t(locale,
|
|
5725
|
-
: t(locale, automatic ?
|
|
5731
|
+
? t(locale, doneQuietKey)
|
|
5732
|
+
: t(locale, automatic ? doneKey : 'auth_switch_done', this.codexAuthSwitchParams(locale, result.fromLabel, result.toLabel))];
|
|
5726
5733
|
if (recovered) {
|
|
5727
5734
|
lines.push(t(locale, 'auth_recovered_newer_candidate', { value: candidate.name }));
|
|
5728
5735
|
}
|
|
@@ -9337,16 +9344,36 @@ function cloneAuthRetryContext(context) {
|
|
|
9337
9344
|
failedAuthTargets: new Set(context.failedAuthTargets),
|
|
9338
9345
|
};
|
|
9339
9346
|
}
|
|
9340
|
-
function
|
|
9341
|
-
|
|
9342
|
-
|
|
9347
|
+
function classifyCodexAuthRotationError(params) {
|
|
9348
|
+
const collected = collectCodexErrorText(params);
|
|
9349
|
+
if (isChatGptBackendAccessBlocked(collected)) {
|
|
9350
|
+
return null;
|
|
9343
9351
|
}
|
|
9344
9352
|
const code = stringOrNull(params?.error?.codexErrorInfo) ?? stringOrNull(params?.error?.code);
|
|
9345
|
-
if (code && /usageLimitExceeded|auth|unauthorized|forbidden|login/i.test(code)) {
|
|
9346
|
-
return true;
|
|
9347
|
-
}
|
|
9348
9353
|
const message = stringOrNull(params?.error?.message) ?? '';
|
|
9349
|
-
|
|
9354
|
+
const text = `${code ?? ''}\n${message}\n${collected}`;
|
|
9355
|
+
if (isCodexQuotaLimitError(text)) {
|
|
9356
|
+
return 'quota_limited';
|
|
9357
|
+
}
|
|
9358
|
+
if (code && /auth|unauthorized|forbidden|login/i.test(code)) {
|
|
9359
|
+
return 'auth_invalid';
|
|
9360
|
+
}
|
|
9361
|
+
return /(not authenticated|unauthorized|forbidden|sign in|log in|login|auth)/i.test(message)
|
|
9362
|
+
? 'auth_invalid'
|
|
9363
|
+
: null;
|
|
9364
|
+
}
|
|
9365
|
+
function isCodexQuotaLimitError(text) {
|
|
9366
|
+
return /usageLimitExceeded/i.test(text)
|
|
9367
|
+
|| /you['’]?ve hit your usage limit/i.test(text)
|
|
9368
|
+
|| /\busage limit(?:s)?\b/i.test(text)
|
|
9369
|
+
|| /\brate limit(?:ed|s)?\b/i.test(text)
|
|
9370
|
+
|| /\btoo many requests\b/i.test(text)
|
|
9371
|
+
|| /\binsufficient[_\s-]?quota\b/i.test(text)
|
|
9372
|
+
|| /\bquota exceeded\b/i.test(text)
|
|
9373
|
+
|| /\bbilling hard limit\b/i.test(text)
|
|
9374
|
+
|| /\bcredits? exhausted\b/i.test(text)
|
|
9375
|
+
|| /\bout of credits?\b/i.test(text)
|
|
9376
|
+
|| /\bcredits? limit\b/i.test(text);
|
|
9350
9377
|
}
|
|
9351
9378
|
function formatCodexNotificationError(params) {
|
|
9352
9379
|
const collected = collectCodexErrorText(params);
|
package/dist/i18n.d.ts
CHANGED
|
@@ -219,9 +219,14 @@ declare const MESSAGES: {
|
|
|
219
219
|
readonly auth_auto_recovered_current: "Codex auth problem detected ({error}). Recovered a newer same-account credential for {value} and restarted Codex app-server.";
|
|
220
220
|
readonly auth_auto_done: "Auto-switched Codex auth: {from} -> {to}.";
|
|
221
221
|
readonly auth_auto_done_quiet: "Codex auth problem handled with another maintained candidate.";
|
|
222
|
+
readonly auth_quota_switching: "Codex usage limit detected ({error}). Switching: {from} -> {to}...";
|
|
223
|
+
readonly auth_quota_switching_quiet: "Codex usage limit detected ({error}). Selecting another maintained candidate...";
|
|
224
|
+
readonly auth_quota_done: "Switched Codex auth after usage limit: {from} -> {to}.";
|
|
225
|
+
readonly auth_quota_done_quiet: "Codex usage limit handled with another maintained candidate.";
|
|
222
226
|
readonly auth_auto_retrying: "Retrying the same request with the new auth...";
|
|
223
227
|
readonly auth_auto_retry_thread_missing: "Auth was switched, but the original thread is no longer available ({threadId}). Retry was stopped to avoid creating duplicate sessions.";
|
|
224
228
|
readonly auth_auto_no_candidate: "Codex auth problem detected ({error}), but no unused auth candidate is available.";
|
|
229
|
+
readonly auth_quota_no_candidate: "Codex usage limit detected ({error}), but no unused auth candidate is available.";
|
|
225
230
|
readonly auth_add_exists: "Auth candidate already exists: {value}";
|
|
226
231
|
readonly auth_add_preparing: "Preparing new Codex auth candidate {value}...";
|
|
227
232
|
readonly auth_add_started: "New auth login started for {value}.";
|
|
@@ -907,9 +912,14 @@ declare const MESSAGES: {
|
|
|
907
912
|
readonly auth_auto_recovered_current: "检测到 Codex auth 问题({error}),已为 {value} 恢复同账号较新凭据并重启 Codex app-server。";
|
|
908
913
|
readonly auth_auto_done: "已自动切换 Codex auth:{from} -> {to}。";
|
|
909
914
|
readonly auth_auto_done_quiet: "Codex auth 问题已用另一个维护中的候选处理。";
|
|
915
|
+
readonly auth_quota_switching: "检测到 Codex 额度限制({error}),正在切换:{from} -> {to}...";
|
|
916
|
+
readonly auth_quota_switching_quiet: "检测到 Codex 额度限制({error}),正在选择另一个维护中的候选...";
|
|
917
|
+
readonly auth_quota_done: "已因 Codex 额度限制自动切换 auth:{from} -> {to}。";
|
|
918
|
+
readonly auth_quota_done_quiet: "Codex 额度限制已用另一个维护中的候选处理。";
|
|
910
919
|
readonly auth_auto_retrying: "正在用新的 auth 重试同一条请求...";
|
|
911
920
|
readonly auth_auto_retry_thread_missing: "Auth 已切换,但原线程已经不可用({threadId})。已停止重试,避免创建重复 session。";
|
|
912
921
|
readonly auth_auto_no_candidate: "检测到 Codex auth 问题({error}),但没有可用的未失败候选 auth。";
|
|
922
|
+
readonly auth_quota_no_candidate: "检测到 Codex 额度限制({error}),但没有可用的未失败候选 auth。";
|
|
913
923
|
readonly auth_add_exists: "auth 候选已经存在:{value}";
|
|
914
924
|
readonly auth_add_preparing: "正在准备新的 Codex auth 候选 {value}...";
|
|
915
925
|
readonly auth_add_started: "{value} 的新 auth 登录已开始。";
|
package/dist/i18n.js
CHANGED
|
@@ -217,9 +217,14 @@ const MESSAGES = {
|
|
|
217
217
|
auth_auto_recovered_current: 'Codex auth problem detected ({error}). Recovered a newer same-account credential for {value} and restarted Codex app-server.',
|
|
218
218
|
auth_auto_done: 'Auto-switched Codex auth: {from} -> {to}.',
|
|
219
219
|
auth_auto_done_quiet: 'Codex auth problem handled with another maintained candidate.',
|
|
220
|
+
auth_quota_switching: 'Codex usage limit detected ({error}). Switching: {from} -> {to}...',
|
|
221
|
+
auth_quota_switching_quiet: 'Codex usage limit detected ({error}). Selecting another maintained candidate...',
|
|
222
|
+
auth_quota_done: 'Switched Codex auth after usage limit: {from} -> {to}.',
|
|
223
|
+
auth_quota_done_quiet: 'Codex usage limit handled with another maintained candidate.',
|
|
220
224
|
auth_auto_retrying: 'Retrying the same request with the new auth...',
|
|
221
225
|
auth_auto_retry_thread_missing: 'Auth was switched, but the original thread is no longer available ({threadId}). Retry was stopped to avoid creating duplicate sessions.',
|
|
222
226
|
auth_auto_no_candidate: 'Codex auth problem detected ({error}), but no unused auth candidate is available.',
|
|
227
|
+
auth_quota_no_candidate: 'Codex usage limit detected ({error}), but no unused auth candidate is available.',
|
|
223
228
|
auth_add_exists: 'Auth candidate already exists: {value}',
|
|
224
229
|
auth_add_preparing: 'Preparing new Codex auth candidate {value}...',
|
|
225
230
|
auth_add_started: 'New auth login started for {value}.',
|
|
@@ -905,9 +910,14 @@ const MESSAGES = {
|
|
|
905
910
|
auth_auto_recovered_current: '检测到 Codex auth 问题({error}),已为 {value} 恢复同账号较新凭据并重启 Codex app-server。',
|
|
906
911
|
auth_auto_done: '已自动切换 Codex auth:{from} -> {to}。',
|
|
907
912
|
auth_auto_done_quiet: 'Codex auth 问题已用另一个维护中的候选处理。',
|
|
913
|
+
auth_quota_switching: '检测到 Codex 额度限制({error}),正在切换:{from} -> {to}...',
|
|
914
|
+
auth_quota_switching_quiet: '检测到 Codex 额度限制({error}),正在选择另一个维护中的候选...',
|
|
915
|
+
auth_quota_done: '已因 Codex 额度限制自动切换 auth:{from} -> {to}。',
|
|
916
|
+
auth_quota_done_quiet: 'Codex 额度限制已用另一个维护中的候选处理。',
|
|
908
917
|
auth_auto_retrying: '正在用新的 auth 重试同一条请求...',
|
|
909
918
|
auth_auto_retry_thread_missing: 'Auth 已切换,但原线程已经不可用({threadId})。已停止重试,避免创建重复 session。',
|
|
910
919
|
auth_auto_no_candidate: '检测到 Codex auth 问题({error}),但没有可用的未失败候选 auth。',
|
|
920
|
+
auth_quota_no_candidate: '检测到 Codex 额度限制({error}),但没有可用的未失败候选 auth。',
|
|
911
921
|
auth_add_exists: 'auth 候选已经存在:{value}',
|
|
912
922
|
auth_add_preparing: '正在准备新的 Codex auth 候选 {value}...',
|
|
913
923
|
auth_add_started: '{value} 的新 auth 登录已开始。',
|