@foxden-app/foxclaw 0.5.21 → 0.5.23

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,26 @@
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.23 - 2026-06-09
6
+
7
+ ### 中文
8
+ - 补齐切换后验证路径:如果目标候选在验证 rate-limit usage 时返回 usage/rate/quota/billing/credits limit,FoxClaw 会恢复到原 auth,但不会把目标候选标记为 `?`,也不会触发自动剔除。
9
+ - 这让额度耗尽在自动轮换、手动切换和验证失败提示里都保持同一语义:它只是暂时没额度,不是凭据失效。
10
+
11
+ ### English
12
+ - Completed the post-switch validation path: if the selected candidate returns a usage/rate/quota/billing/credits limit while validating rate-limit usage, FoxClaw restores the previous auth but does not mark the selected candidate `?` or auto-delete it.
13
+ - Quota exhaustion now has the same meaning across automatic rotation, manual switching, and validation failure messages: temporarily out of quota, not an invalid credential.
14
+
15
+ ## 0.5.22 - 2026-06-09
16
+
17
+ ### 中文
18
+ - Codex 报 `usageLimitExceeded`、`You've hit your usage limit`、usage/rate/quota/billing/credits limit 这类额度耗尽时,不再把当前 auth 候选标记为需要修复,也不会触发 `AUTH_AUTO_DELETE_NEEDS_REPAIR` 的自动剔除和跨节点删除。
19
+ - 额度耗尽仍会临时避开当前候选,切到另一个维护中的候选重试;提示文案改为“Codex 额度限制”,不再误写成 “Codex auth 问题”。
20
+
21
+ ### English
22
+ - 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.
23
+ - 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.
24
+
5
25
  ## 0.5.21 - 2026-06-09
6
26
 
7
27
  ### 中文
@@ -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);
@@ -5688,13 +5693,19 @@ export class BridgeSessionCore {
5688
5693
  error: toErrorMeta(error),
5689
5694
  });
5690
5695
  }
5691
- const repairDisposition = await this.markCodexAuthCandidateNeedsRepair(candidate.name);
5696
+ const validationFailureKind = isCodexQuotaLimitError(validation.error)
5697
+ ? 'quota_limited'
5698
+ : 'auth_invalid';
5699
+ const repairDisposition = validationFailureKind === 'auth_invalid'
5700
+ ? await this.markCodexAuthCandidateNeedsRepair(candidate.name)
5701
+ : { deleted: false, restarted: false };
5692
5702
  const outcome = {
5693
5703
  ...result,
5694
5704
  ok: false,
5695
5705
  candidateName: candidate.name,
5696
5706
  recovered,
5697
5707
  error: validation.error,
5708
+ validationFailureKind,
5698
5709
  restoredPrevious,
5699
5710
  autoDeleted: repairDisposition.deleted,
5700
5711
  deleteRestarted: repairDisposition.restarted,
@@ -5713,6 +5724,7 @@ export class BridgeSessionCore {
5713
5724
  candidateName: candidate.name,
5714
5725
  recovered,
5715
5726
  error: null,
5727
+ validationFailureKind: null,
5716
5728
  restoredPrevious: false,
5717
5729
  autoDeleted: false,
5718
5730
  deleteRestarted: false,
@@ -5720,9 +5732,11 @@ export class BridgeSessionCore {
5720
5732
  if (!sendResult) {
5721
5733
  return outcome;
5722
5734
  }
5735
+ const doneKey = automaticReason === 'quota_limited' ? 'auth_quota_done' : 'auth_auto_done';
5736
+ const doneQuietKey = automaticReason === 'quota_limited' ? 'auth_quota_done_quiet' : 'auth_auto_done_quiet';
5723
5737
  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))];
5738
+ ? t(locale, doneQuietKey)
5739
+ : t(locale, automatic ? doneKey : 'auth_switch_done', this.codexAuthSwitchParams(locale, result.fromLabel, result.toLabel))];
5726
5740
  if (recovered) {
5727
5741
  lines.push(t(locale, 'auth_recovered_newer_candidate', { value: candidate.name }));
5728
5742
  }
@@ -5771,11 +5785,13 @@ export class BridgeSessionCore {
5771
5785
  if (outcome.ok) {
5772
5786
  return [];
5773
5787
  }
5774
- const validationKey = outcome.autoDeleted && this.config.authAutoDeleteNeedsRepair
5775
- ? 'auth_switch_validation_auto_deleted_quiet'
5776
- : outcome.autoDeleted
5777
- ? 'auth_switch_validation_auto_deleted'
5778
- : 'auth_switch_validation_failed';
5788
+ const validationKey = outcome.validationFailureKind === 'quota_limited'
5789
+ ? 'auth_switch_validation_quota_limited'
5790
+ : outcome.autoDeleted && this.config.authAutoDeleteNeedsRepair
5791
+ ? 'auth_switch_validation_auto_deleted_quiet'
5792
+ : outcome.autoDeleted
5793
+ ? 'auth_switch_validation_auto_deleted'
5794
+ : 'auth_switch_validation_failed';
5779
5795
  return [
5780
5796
  t(locale, validationKey, {
5781
5797
  value: outcome.candidateName,
@@ -9337,16 +9353,36 @@ function cloneAuthRetryContext(context) {
9337
9353
  failedAuthTargets: new Set(context.failedAuthTargets),
9338
9354
  };
9339
9355
  }
9340
- function isCodexAuthRotationError(params) {
9341
- if (isChatGptBackendAccessBlocked(collectCodexErrorText(params))) {
9342
- return false;
9356
+ function classifyCodexAuthRotationError(params) {
9357
+ const collected = collectCodexErrorText(params);
9358
+ if (isChatGptBackendAccessBlocked(collected)) {
9359
+ return null;
9343
9360
  }
9344
9361
  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
9362
  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);
9363
+ const text = `${code ?? ''}\n${message}\n${collected}`;
9364
+ if (isCodexQuotaLimitError(text)) {
9365
+ return 'quota_limited';
9366
+ }
9367
+ if (code && /auth|unauthorized|forbidden|login/i.test(code)) {
9368
+ return 'auth_invalid';
9369
+ }
9370
+ return /(not authenticated|unauthorized|forbidden|sign in|log in|login|auth)/i.test(message)
9371
+ ? 'auth_invalid'
9372
+ : null;
9373
+ }
9374
+ function isCodexQuotaLimitError(text) {
9375
+ return /usageLimitExceeded/i.test(text)
9376
+ || /you['’]?ve hit your usage limit/i.test(text)
9377
+ || /\busage limit(?:s)?\b/i.test(text)
9378
+ || /\brate limit(?:ed|s)?\b/i.test(text)
9379
+ || /\btoo many requests\b/i.test(text)
9380
+ || /\binsufficient[_\s-]?quota\b/i.test(text)
9381
+ || /\bquota exceeded\b/i.test(text)
9382
+ || /\bbilling hard limit\b/i.test(text)
9383
+ || /\bcredits? exhausted\b/i.test(text)
9384
+ || /\bout of credits?\b/i.test(text)
9385
+ || /\bcredits? limit\b/i.test(text);
9350
9386
  }
9351
9387
  function formatCodexNotificationError(params) {
9352
9388
  const collected = collectCodexErrorText(params);
package/dist/i18n.d.ts CHANGED
@@ -199,6 +199,7 @@ declare const MESSAGES: {
199
199
  readonly auth_switch_validation_failed: "Selected auth failed validation: {error}. Marked {value} for login repair.";
200
200
  readonly auth_switch_validation_auto_deleted: "Selected auth failed validation: {error}. Auto-deleted {value}.";
201
201
  readonly auth_switch_validation_auto_deleted_quiet: "Selected auth failed validation and was auto-deleted.";
202
+ readonly auth_switch_validation_quota_limited: "Selected auth hit a Codex usage limit: {error}. It was not marked invalid.";
202
203
  readonly auth_switch_validation_reverted: "Restored the previous auth after the failed switch.";
203
204
  readonly auth_recovered_newer_candidate: "Recovered a newer same-account credential for {value} from another Codex home before reload.";
204
205
  readonly auth_refresh_all_confirm_short: "Review refresh all risk first.";
@@ -219,9 +220,14 @@ declare const MESSAGES: {
219
220
  readonly auth_auto_recovered_current: "Codex auth problem detected ({error}). Recovered a newer same-account credential for {value} and restarted Codex app-server.";
220
221
  readonly auth_auto_done: "Auto-switched Codex auth: {from} -> {to}.";
221
222
  readonly auth_auto_done_quiet: "Codex auth problem handled with another maintained candidate.";
223
+ readonly auth_quota_switching: "Codex usage limit detected ({error}). Switching: {from} -> {to}...";
224
+ readonly auth_quota_switching_quiet: "Codex usage limit detected ({error}). Selecting another maintained candidate...";
225
+ readonly auth_quota_done: "Switched Codex auth after usage limit: {from} -> {to}.";
226
+ readonly auth_quota_done_quiet: "Codex usage limit handled with another maintained candidate.";
222
227
  readonly auth_auto_retrying: "Retrying the same request with the new auth...";
223
228
  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
229
  readonly auth_auto_no_candidate: "Codex auth problem detected ({error}), but no unused auth candidate is available.";
230
+ readonly auth_quota_no_candidate: "Codex usage limit detected ({error}), but no unused auth candidate is available.";
225
231
  readonly auth_add_exists: "Auth candidate already exists: {value}";
226
232
  readonly auth_add_preparing: "Preparing new Codex auth candidate {value}...";
227
233
  readonly auth_add_started: "New auth login started for {value}.";
@@ -887,6 +893,7 @@ declare const MESSAGES: {
887
893
  readonly auth_switch_validation_failed: "选中的 auth 验证失败:{error}。已将 {value} 标记为需要登录修复。";
888
894
  readonly auth_switch_validation_auto_deleted: "选中的 auth 验证失败:{error}。已自动剔除 {value}。";
889
895
  readonly auth_switch_validation_auto_deleted_quiet: "选中的 auth 验证失败,已自动剔除。";
896
+ readonly auth_switch_validation_quota_limited: "选中的 auth 已触发 Codex 额度限制:{error}。没有把它标记为失效。";
890
897
  readonly auth_switch_validation_reverted: "已在切换失败后恢复到之前的 auth。";
891
898
  readonly auth_recovered_newer_candidate: "重载前已从其他 Codex home 恢复 {value} 的同账号较新凭据。";
892
899
  readonly auth_refresh_all_confirm_short: "请先确认刷新全部风险。";
@@ -907,9 +914,14 @@ declare const MESSAGES: {
907
914
  readonly auth_auto_recovered_current: "检测到 Codex auth 问题({error}),已为 {value} 恢复同账号较新凭据并重启 Codex app-server。";
908
915
  readonly auth_auto_done: "已自动切换 Codex auth:{from} -> {to}。";
909
916
  readonly auth_auto_done_quiet: "Codex auth 问题已用另一个维护中的候选处理。";
917
+ readonly auth_quota_switching: "检测到 Codex 额度限制({error}),正在切换:{from} -> {to}...";
918
+ readonly auth_quota_switching_quiet: "检测到 Codex 额度限制({error}),正在选择另一个维护中的候选...";
919
+ readonly auth_quota_done: "已因 Codex 额度限制自动切换 auth:{from} -> {to}。";
920
+ readonly auth_quota_done_quiet: "Codex 额度限制已用另一个维护中的候选处理。";
910
921
  readonly auth_auto_retrying: "正在用新的 auth 重试同一条请求...";
911
922
  readonly auth_auto_retry_thread_missing: "Auth 已切换,但原线程已经不可用({threadId})。已停止重试,避免创建重复 session。";
912
923
  readonly auth_auto_no_candidate: "检测到 Codex auth 问题({error}),但没有可用的未失败候选 auth。";
924
+ readonly auth_quota_no_candidate: "检测到 Codex 额度限制({error}),但没有可用的未失败候选 auth。";
913
925
  readonly auth_add_exists: "auth 候选已经存在:{value}";
914
926
  readonly auth_add_preparing: "正在准备新的 Codex auth 候选 {value}...";
915
927
  readonly auth_add_started: "{value} 的新 auth 登录已开始。";
package/dist/i18n.js CHANGED
@@ -197,6 +197,7 @@ const MESSAGES = {
197
197
  auth_switch_validation_failed: 'Selected auth failed validation: {error}. Marked {value} for login repair.',
198
198
  auth_switch_validation_auto_deleted: 'Selected auth failed validation: {error}. Auto-deleted {value}.',
199
199
  auth_switch_validation_auto_deleted_quiet: 'Selected auth failed validation and was auto-deleted.',
200
+ auth_switch_validation_quota_limited: 'Selected auth hit a Codex usage limit: {error}. It was not marked invalid.',
200
201
  auth_switch_validation_reverted: 'Restored the previous auth after the failed switch.',
201
202
  auth_recovered_newer_candidate: 'Recovered a newer same-account credential for {value} from another Codex home before reload.',
202
203
  auth_refresh_all_confirm_short: 'Review refresh all risk first.',
@@ -217,9 +218,14 @@ const MESSAGES = {
217
218
  auth_auto_recovered_current: 'Codex auth problem detected ({error}). Recovered a newer same-account credential for {value} and restarted Codex app-server.',
218
219
  auth_auto_done: 'Auto-switched Codex auth: {from} -> {to}.',
219
220
  auth_auto_done_quiet: 'Codex auth problem handled with another maintained candidate.',
221
+ auth_quota_switching: 'Codex usage limit detected ({error}). Switching: {from} -> {to}...',
222
+ auth_quota_switching_quiet: 'Codex usage limit detected ({error}). Selecting another maintained candidate...',
223
+ auth_quota_done: 'Switched Codex auth after usage limit: {from} -> {to}.',
224
+ auth_quota_done_quiet: 'Codex usage limit handled with another maintained candidate.',
220
225
  auth_auto_retrying: 'Retrying the same request with the new auth...',
221
226
  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
227
  auth_auto_no_candidate: 'Codex auth problem detected ({error}), but no unused auth candidate is available.',
228
+ auth_quota_no_candidate: 'Codex usage limit detected ({error}), but no unused auth candidate is available.',
223
229
  auth_add_exists: 'Auth candidate already exists: {value}',
224
230
  auth_add_preparing: 'Preparing new Codex auth candidate {value}...',
225
231
  auth_add_started: 'New auth login started for {value}.',
@@ -885,6 +891,7 @@ const MESSAGES = {
885
891
  auth_switch_validation_failed: '选中的 auth 验证失败:{error}。已将 {value} 标记为需要登录修复。',
886
892
  auth_switch_validation_auto_deleted: '选中的 auth 验证失败:{error}。已自动剔除 {value}。',
887
893
  auth_switch_validation_auto_deleted_quiet: '选中的 auth 验证失败,已自动剔除。',
894
+ auth_switch_validation_quota_limited: '选中的 auth 已触发 Codex 额度限制:{error}。没有把它标记为失效。',
888
895
  auth_switch_validation_reverted: '已在切换失败后恢复到之前的 auth。',
889
896
  auth_recovered_newer_candidate: '重载前已从其他 Codex home 恢复 {value} 的同账号较新凭据。',
890
897
  auth_refresh_all_confirm_short: '请先确认刷新全部风险。',
@@ -905,9 +912,14 @@ const MESSAGES = {
905
912
  auth_auto_recovered_current: '检测到 Codex auth 问题({error}),已为 {value} 恢复同账号较新凭据并重启 Codex app-server。',
906
913
  auth_auto_done: '已自动切换 Codex auth:{from} -> {to}。',
907
914
  auth_auto_done_quiet: 'Codex auth 问题已用另一个维护中的候选处理。',
915
+ auth_quota_switching: '检测到 Codex 额度限制({error}),正在切换:{from} -> {to}...',
916
+ auth_quota_switching_quiet: '检测到 Codex 额度限制({error}),正在选择另一个维护中的候选...',
917
+ auth_quota_done: '已因 Codex 额度限制自动切换 auth:{from} -> {to}。',
918
+ auth_quota_done_quiet: 'Codex 额度限制已用另一个维护中的候选处理。',
908
919
  auth_auto_retrying: '正在用新的 auth 重试同一条请求...',
909
920
  auth_auto_retry_thread_missing: 'Auth 已切换,但原线程已经不可用({threadId})。已停止重试,避免创建重复 session。',
910
921
  auth_auto_no_candidate: '检测到 Codex auth 问题({error}),但没有可用的未失败候选 auth。',
922
+ auth_quota_no_candidate: '检测到 Codex 额度限制({error}),但没有可用的未失败候选 auth。',
911
923
  auth_add_exists: 'auth 候选已经存在:{value}',
912
924
  auth_add_preparing: '正在准备新的 Codex auth 候选 {value}...',
913
925
  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.23",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",