@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 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 isAuthRotationError = isCodexAuthRotationError(params);
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 (isAuthRotationError && !willRetry && active?.authRetry) {
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, 'auth_auto_switching_quiet', { error: formatShortStatusError(rotation.reason) })
5539
- : t(locale, 'auth_auto_switching', {
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, 'auth_auto_done_quiet')
5725
- : t(locale, automatic ? 'auth_auto_done' : 'auth_switch_done', this.codexAuthSwitchParams(locale, result.fromLabel, result.toLabel))];
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 isCodexAuthRotationError(params) {
9341
- if (isChatGptBackendAccessBlocked(collectCodexErrorText(params))) {
9342
- return false;
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
- return /(usage limit|rate limit|not authenticated|unauthorized|forbidden|sign in|log in|login|auth)/i.test(message);
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 登录已开始。',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.5.21",
3
+ "version": "0.5.22",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",