@foxden-app/foxclaw 0.5.20 → 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/.env.example +4 -0
- package/CHANGELOG.md +24 -0
- package/README.md +1 -1
- package/dist/auth/cross_node_sync.d.ts +55 -0
- package/dist/auth/cross_node_sync.js +199 -2
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -0
- package/dist/controller/controller.d.ts +5 -1
- package/dist/controller/controller.js +217 -28
- package/dist/i18n.d.ts +36 -0
- package/dist/i18n.js +36 -0
- package/dist/main.js +141 -7
- package/dist/store/database.d.ts +9 -0
- package/dist/store/database.js +87 -0
- package/docs/cross-node-auth-sync.md +4 -0
- package/docs/user-manual.md +3 -0
- package/docs/zh/cross-node-auth-sync.md +4 -0
- package/docs/zh/user-manual.md +3 -0
- package/package.json +1 -1
|
@@ -19,6 +19,7 @@ import { diffObservedTurn, findLatestTurn, findLiveTurn } from './observer.js';
|
|
|
19
19
|
import { applySessionLog, bootstrapSessionLog, splitJsonlChunk, } from './session_observer.js';
|
|
20
20
|
import { renderActiveTurnStatus } from './status.js';
|
|
21
21
|
import { writeRuntimeStatus } from '../runtime.js';
|
|
22
|
+
const AUTH_DELETE_REASON_NEEDS_REPAIR = 'needs_repair';
|
|
22
23
|
class UserFacingError extends Error {
|
|
23
24
|
}
|
|
24
25
|
const OBSERVED_THREAD_POLL_MS = 1500;
|
|
@@ -432,6 +433,7 @@ export class BridgeSessionCore {
|
|
|
432
433
|
t(locale, 'status_pending_user_inputs', { value: this.store.countPendingUserInputs() }),
|
|
433
434
|
t(locale, 'status_queued_turns', { value: this.store.countQueuedTurnInputs(scopeId) }),
|
|
434
435
|
t(locale, 'status_active_turns', { value: this.activeTurns.size }),
|
|
436
|
+
formatCodexAuthPoolSummary(locale, this.store.getCodexAuthPoolStats()),
|
|
435
437
|
];
|
|
436
438
|
if (serviceStatus) {
|
|
437
439
|
lines.push('', t(locale, 'status_runtime_overview'));
|
|
@@ -755,7 +757,7 @@ export class BridgeSessionCore {
|
|
|
755
757
|
return;
|
|
756
758
|
}
|
|
757
759
|
case 'config': {
|
|
758
|
-
await this.handleConfigCommand(scopeId, locale);
|
|
760
|
+
await this.handleConfigCommand(scopeId, locale, args);
|
|
759
761
|
return;
|
|
760
762
|
}
|
|
761
763
|
case 'requirements': {
|
|
@@ -1053,6 +1055,11 @@ export class BridgeSessionCore {
|
|
|
1053
1055
|
await this.handleSetupCallback(event, setupMatch[1], setupMatch[2], locale);
|
|
1054
1056
|
return;
|
|
1055
1057
|
}
|
|
1058
|
+
const configMatch = /^config:auth_auto_delete:(on|off)$/.exec(event.data);
|
|
1059
|
+
if (configMatch) {
|
|
1060
|
+
await this.handleConfigToggleCallback(event, configMatch[1] === 'on', locale);
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1056
1063
|
const settingsMatch = /^settings:(model|effort|access):(.+)$/.exec(event.data);
|
|
1057
1064
|
if (settingsMatch) {
|
|
1058
1065
|
await this.handleSettingsCallback(event, settingsMatch[1], settingsMatch[2], locale);
|
|
@@ -1383,7 +1390,8 @@ export class BridgeSessionCore {
|
|
|
1383
1390
|
: threadId
|
|
1384
1391
|
? this.findActiveTurnsByThreadId(threadId)
|
|
1385
1392
|
: [];
|
|
1386
|
-
const
|
|
1393
|
+
const authRotationReason = classifyCodexAuthRotationError(params);
|
|
1394
|
+
const isAuthRotationError = authRotationReason !== null;
|
|
1387
1395
|
const willRetry = params?.willRetry === true;
|
|
1388
1396
|
const active = isAuthRotationError
|
|
1389
1397
|
? activeTurns.find(turn => turn.authRetry !== null) ?? activeTurns.find(turn => !turn.isObserved) ?? activeTurns[0] ?? null
|
|
@@ -1396,12 +1404,13 @@ export class BridgeSessionCore {
|
|
|
1396
1404
|
else if (turnId) {
|
|
1397
1405
|
this.pendingTurnErrors.set(turnId, message);
|
|
1398
1406
|
}
|
|
1399
|
-
if (
|
|
1407
|
+
if (authRotationReason && !willRetry && active?.authRetry) {
|
|
1400
1408
|
const scopeId = active.scopeId;
|
|
1401
1409
|
if (scopeId) {
|
|
1402
1410
|
this.pendingAuthRotation = {
|
|
1403
1411
|
scopeId,
|
|
1404
1412
|
reason: message,
|
|
1413
|
+
reasonKind: authRotationReason,
|
|
1405
1414
|
retry: cloneAuthRetryContext(active.authRetry),
|
|
1406
1415
|
};
|
|
1407
1416
|
}
|
|
@@ -2931,6 +2940,19 @@ export class BridgeSessionCore {
|
|
|
2931
2940
|
async getCurrentAuthLabel() {
|
|
2932
2941
|
return (await this.listCodexAuthState()).currentLabel;
|
|
2933
2942
|
}
|
|
2943
|
+
async handleExternalCodexAuthCandidateDeleted(candidateName, reason = null) {
|
|
2944
|
+
this.store.deleteCodexAuthCandidate(candidateName);
|
|
2945
|
+
if (isInvalidCodexAuthDeleteReason(reason)) {
|
|
2946
|
+
this.store.recordCodexAuthCandidateInvalidDelete(candidateName, reason);
|
|
2947
|
+
}
|
|
2948
|
+
else {
|
|
2949
|
+
this.store.recordCodexAuthCandidateRemoved(candidateName, reason);
|
|
2950
|
+
}
|
|
2951
|
+
this.authRotationFailedTargets.delete(path.join(this.resolveAuthDir(), candidateName));
|
|
2952
|
+
this.pendingTurnErrors.clear();
|
|
2953
|
+
this.attachedThreads.clear();
|
|
2954
|
+
await this.app.restart();
|
|
2955
|
+
}
|
|
2934
2956
|
async validateExternalCodexAuthCandidate(candidateName, rawAuth, expectedAccountId) {
|
|
2935
2957
|
if (this.externalAuthValidationInProgress) {
|
|
2936
2958
|
return { ok: false, reason: 'runtime is not idle' };
|
|
@@ -4761,13 +4783,13 @@ export class BridgeSessionCore {
|
|
|
4761
4783
|
throw error;
|
|
4762
4784
|
}
|
|
4763
4785
|
}
|
|
4764
|
-
async deleteCodexAuthCandidate(candidate) {
|
|
4786
|
+
async deleteCodexAuthCandidate(candidate, reason = null) {
|
|
4765
4787
|
const wasCurrent = candidate.isCurrent;
|
|
4766
4788
|
const authDir = this.resolveAuthDir();
|
|
4767
4789
|
const authPath = path.join(authDir, 'auth.json');
|
|
4768
4790
|
let deletedByCoordinator = false;
|
|
4769
4791
|
try {
|
|
4770
|
-
await this.coordinator?.authCandidateDeleted?.(this.authRuntimeId(), candidate.name);
|
|
4792
|
+
await this.coordinator?.authCandidateDeleted?.(this.authRuntimeId(), candidate.name, reason);
|
|
4771
4793
|
deletedByCoordinator = Boolean(this.coordinator?.authCandidateDeleted);
|
|
4772
4794
|
}
|
|
4773
4795
|
catch (error) {
|
|
@@ -4784,6 +4806,12 @@ export class BridgeSessionCore {
|
|
|
4784
4806
|
}
|
|
4785
4807
|
}
|
|
4786
4808
|
this.store.deleteCodexAuthCandidate(candidate.name);
|
|
4809
|
+
if (isInvalidCodexAuthDeleteReason(reason)) {
|
|
4810
|
+
this.store.recordCodexAuthCandidateInvalidDelete(candidate.name, reason);
|
|
4811
|
+
}
|
|
4812
|
+
else {
|
|
4813
|
+
this.store.recordCodexAuthCandidateRemoved(candidate.name, reason);
|
|
4814
|
+
}
|
|
4787
4815
|
this.authRotationFailedTargets.delete(candidate.path);
|
|
4788
4816
|
const snapshots = await this.readCodexAuthQuotaSnapshots();
|
|
4789
4817
|
if (Object.prototype.hasOwnProperty.call(snapshots, candidate.name)) {
|
|
@@ -5092,10 +5120,58 @@ export class BridgeSessionCore {
|
|
|
5092
5120
|
const features = await this.app.listExperimentalFeatures();
|
|
5093
5121
|
await this.sendMessage(scopeId, formatFeaturesMessage(locale, features));
|
|
5094
5122
|
}
|
|
5095
|
-
async handleConfigCommand(scopeId, locale) {
|
|
5123
|
+
async handleConfigCommand(scopeId, locale, args = []) {
|
|
5124
|
+
const action = args[0]?.toLowerCase() ?? '';
|
|
5125
|
+
if (['auth_auto_delete', 'auth-auto-delete', 'auto_delete_needs_repair', 'auto-delete-needs-repair'].includes(action)) {
|
|
5126
|
+
const enabled = parseConfigBooleanArg(args[1]);
|
|
5127
|
+
if (enabled === null) {
|
|
5128
|
+
await this.sendMessage(scopeId, t(locale, 'config_auth_auto_delete_usage'));
|
|
5129
|
+
return;
|
|
5130
|
+
}
|
|
5131
|
+
const update = await this.setAuthAutoDeleteNeedsRepair(enabled);
|
|
5132
|
+
const binding = this.store.getBinding(scopeId);
|
|
5133
|
+
const result = await this.app.readConfig(binding?.cwd ?? this.config.defaultCwd, true);
|
|
5134
|
+
await this.sendMessage(scopeId, `${this.formatConfigToggleUpdate(locale, update)}\n\n${formatConfigMessage(locale, result, this.config, this.store.getCodexAuthPoolStats())}`, configKeyboard(locale, this.config));
|
|
5135
|
+
return;
|
|
5136
|
+
}
|
|
5096
5137
|
const binding = this.store.getBinding(scopeId);
|
|
5097
5138
|
const result = await this.app.readConfig(binding?.cwd ?? this.config.defaultCwd, true);
|
|
5098
|
-
await this.sendMessage(scopeId, formatConfigMessage(locale, result));
|
|
5139
|
+
await this.sendMessage(scopeId, formatConfigMessage(locale, result, this.config, this.store.getCodexAuthPoolStats()), configKeyboard(locale, this.config));
|
|
5140
|
+
}
|
|
5141
|
+
async handleConfigToggleCallback(event, enabled, locale) {
|
|
5142
|
+
const update = await this.setAuthAutoDeleteNeedsRepair(enabled);
|
|
5143
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'decision_recorded'));
|
|
5144
|
+
const binding = this.store.getBinding(event.scopeId);
|
|
5145
|
+
const result = await this.app.readConfig(binding?.cwd ?? this.config.defaultCwd, true);
|
|
5146
|
+
const message = `${this.formatConfigToggleUpdate(locale, update)}\n\n${formatConfigMessage(locale, result, this.config, this.store.getCodexAuthPoolStats())}`;
|
|
5147
|
+
if (event.messageId !== null) {
|
|
5148
|
+
await this.editMessage(event.scopeId, event.messageId, message, configKeyboard(locale, this.config));
|
|
5149
|
+
}
|
|
5150
|
+
else {
|
|
5151
|
+
await this.sendMessage(event.scopeId, message, configKeyboard(locale, this.config));
|
|
5152
|
+
}
|
|
5153
|
+
}
|
|
5154
|
+
async setAuthAutoDeleteNeedsRepair(enabled) {
|
|
5155
|
+
this.config.authAutoDeleteNeedsRepair = enabled;
|
|
5156
|
+
const envPath = this.config.envPath;
|
|
5157
|
+
if (!envPath) {
|
|
5158
|
+
return { enabled, envPath: null, envUpdated: false, envError: null };
|
|
5159
|
+
}
|
|
5160
|
+
try {
|
|
5161
|
+
await writeEnvBoolean(envPath, 'AUTH_AUTO_DELETE_NEEDS_REPAIR', enabled);
|
|
5162
|
+
return { enabled, envPath, envUpdated: true, envError: null };
|
|
5163
|
+
}
|
|
5164
|
+
catch (error) {
|
|
5165
|
+
this.logger.warn('config.env_update_failed', { key: 'AUTH_AUTO_DELETE_NEEDS_REPAIR', envPath, error: toErrorMeta(error) });
|
|
5166
|
+
return { enabled, envPath, envUpdated: false, envError: formatUserError(error) };
|
|
5167
|
+
}
|
|
5168
|
+
}
|
|
5169
|
+
formatConfigToggleUpdate(locale, update) {
|
|
5170
|
+
const lines = [t(locale, 'config_auth_auto_delete_updated', { value: t(locale, update.enabled ? 'yes' : 'no') })];
|
|
5171
|
+
if (update.envError) {
|
|
5172
|
+
lines.push(t(locale, 'config_env_update_failed', { value: update.envPath ?? t(locale, 'unknown'), error: update.envError }));
|
|
5173
|
+
}
|
|
5174
|
+
return lines.join('\n');
|
|
5099
5175
|
}
|
|
5100
5176
|
async handleRequirementsCommand(scopeId, locale) {
|
|
5101
5177
|
const requirements = await this.app.readConfigRequirements();
|
|
@@ -5430,7 +5506,7 @@ export class BridgeSessionCore {
|
|
|
5430
5506
|
const failedTargets = rotation.retry?.failedAuthTargets ?? this.authRotationFailedTargets;
|
|
5431
5507
|
const locale = this.localeForChat(rotation.scopeId);
|
|
5432
5508
|
const current = (await this.listCodexAuthState()).candidates.find(candidate => candidate.isCurrent) ?? null;
|
|
5433
|
-
if (current) {
|
|
5509
|
+
if (current && rotation.reasonKind === 'auth_invalid') {
|
|
5434
5510
|
const recoveredCurrent = await this.recoverCodexAuthCandidate(current.name);
|
|
5435
5511
|
if (recoveredCurrent) {
|
|
5436
5512
|
await this.sendMessage(rotation.scopeId, t(locale, 'auth_auto_recovered_current', {
|
|
@@ -5447,21 +5523,29 @@ export class BridgeSessionCore {
|
|
|
5447
5523
|
}
|
|
5448
5524
|
return false;
|
|
5449
5525
|
}
|
|
5450
|
-
this.markCodexAuthCandidateNeedsRepair(current.name);
|
|
5526
|
+
const disposition = await this.markCodexAuthCandidateNeedsRepair(current.name);
|
|
5527
|
+
if (disposition.deleted) {
|
|
5528
|
+
await this.sendMessage(rotation.scopeId, formatCodexAuthPoolSummary(locale, this.store.getCodexAuthPoolStats()));
|
|
5529
|
+
}
|
|
5451
5530
|
}
|
|
5452
5531
|
const selection = await this.selectNextCodexAuthCandidate(failedTargets);
|
|
5453
5532
|
if (!selection) {
|
|
5454
|
-
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', {
|
|
5455
5534
|
error: formatShortStatusError(rotation.reason),
|
|
5456
5535
|
}));
|
|
5457
5536
|
return false;
|
|
5458
5537
|
}
|
|
5459
5538
|
const { candidate, fromLabel, toLabel } = selection;
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
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');
|
|
5542
|
+
await this.sendMessage(rotation.scopeId, this.config.authAutoDeleteNeedsRepair
|
|
5543
|
+
? t(locale, switchingKey, { error: formatShortStatusError(rotation.reason) })
|
|
5544
|
+
: t(locale, switchingKey, {
|
|
5545
|
+
...this.codexAuthSwitchParams(locale, fromLabel, toLabel),
|
|
5546
|
+
error: formatShortStatusError(rotation.reason),
|
|
5547
|
+
}));
|
|
5548
|
+
const outcome = await this.switchCodexAuthAndRestart(rotation.scopeId, locale, candidate, true, true, rotation.reasonKind);
|
|
5465
5549
|
if (!outcome.ok) {
|
|
5466
5550
|
return false;
|
|
5467
5551
|
}
|
|
@@ -5548,6 +5632,7 @@ export class BridgeSessionCore {
|
|
|
5548
5632
|
}
|
|
5549
5633
|
async listCodexAuthState() {
|
|
5550
5634
|
const state = await listCodexAuthState(this.store.listDisabledCodexAuthCandidateNames(this.authRuntimeId()), this.store.listCodexAuthCandidateStates(this.authRuntimeId()), this.resolveAuthDir());
|
|
5635
|
+
this.store.recordCodexAuthPoolInventory(state.candidates.map(candidate => candidate.name));
|
|
5551
5636
|
const snapshots = await this.readCodexAuthQuotaSnapshots();
|
|
5552
5637
|
const candidateQuotaIdentities = await this.readCodexAuthCandidateQuotaIdentities(state.candidates);
|
|
5553
5638
|
state.candidates.forEach((candidate) => {
|
|
@@ -5579,7 +5664,7 @@ export class BridgeSessionCore {
|
|
|
5579
5664
|
to: toLabel,
|
|
5580
5665
|
};
|
|
5581
5666
|
}
|
|
5582
|
-
async switchCodexAuthAndRestart(scopeId, locale, candidate, automatic, sendResult = true) {
|
|
5667
|
+
async switchCodexAuthAndRestart(scopeId, locale, candidate, automatic, sendResult = true, automaticReason = 'auth_invalid') {
|
|
5583
5668
|
const authDir = this.resolveAuthDir();
|
|
5584
5669
|
const initialState = await listCodexAuthState(new Set(), new Map(), authDir);
|
|
5585
5670
|
const authStat = await fs.lstat(initialState.authPath).catch(() => null);
|
|
@@ -5594,7 +5679,6 @@ export class BridgeSessionCore {
|
|
|
5594
5679
|
await this.app.restart();
|
|
5595
5680
|
const validation = await this.validateCurrentCodexAuthCandidate(candidate);
|
|
5596
5681
|
if (!validation.ok) {
|
|
5597
|
-
this.markCodexAuthCandidateNeedsRepair(candidate.name);
|
|
5598
5682
|
let restoredPrevious = false;
|
|
5599
5683
|
try {
|
|
5600
5684
|
await restoreCodexAuthTarget(initialState.authDir, initialState.authPath, initialState.currentTargetPath, originalRegularAuth);
|
|
@@ -5609,6 +5693,7 @@ export class BridgeSessionCore {
|
|
|
5609
5693
|
error: toErrorMeta(error),
|
|
5610
5694
|
});
|
|
5611
5695
|
}
|
|
5696
|
+
const repairDisposition = await this.markCodexAuthCandidateNeedsRepair(candidate.name);
|
|
5612
5697
|
const outcome = {
|
|
5613
5698
|
...result,
|
|
5614
5699
|
ok: false,
|
|
@@ -5616,6 +5701,8 @@ export class BridgeSessionCore {
|
|
|
5616
5701
|
recovered,
|
|
5617
5702
|
error: validation.error,
|
|
5618
5703
|
restoredPrevious,
|
|
5704
|
+
autoDeleted: repairDisposition.deleted,
|
|
5705
|
+
deleteRestarted: repairDisposition.restarted,
|
|
5619
5706
|
};
|
|
5620
5707
|
if (sendResult) {
|
|
5621
5708
|
const lines = this.formatAuthSwitchValidationLines(locale, outcome);
|
|
@@ -5632,11 +5719,17 @@ export class BridgeSessionCore {
|
|
|
5632
5719
|
recovered,
|
|
5633
5720
|
error: null,
|
|
5634
5721
|
restoredPrevious: false,
|
|
5722
|
+
autoDeleted: false,
|
|
5723
|
+
deleteRestarted: false,
|
|
5635
5724
|
};
|
|
5636
5725
|
if (!sendResult) {
|
|
5637
5726
|
return outcome;
|
|
5638
5727
|
}
|
|
5639
|
-
const
|
|
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';
|
|
5730
|
+
const lines = [automatic && this.config.authAutoDeleteNeedsRepair
|
|
5731
|
+
? t(locale, doneQuietKey)
|
|
5732
|
+
: t(locale, automatic ? doneKey : 'auth_switch_done', this.codexAuthSwitchParams(locale, result.fromLabel, result.toLabel))];
|
|
5640
5733
|
if (recovered) {
|
|
5641
5734
|
lines.push(t(locale, 'auth_recovered_newer_candidate', { value: candidate.name }));
|
|
5642
5735
|
}
|
|
@@ -5685,12 +5778,19 @@ export class BridgeSessionCore {
|
|
|
5685
5778
|
if (outcome.ok) {
|
|
5686
5779
|
return [];
|
|
5687
5780
|
}
|
|
5781
|
+
const validationKey = outcome.autoDeleted && this.config.authAutoDeleteNeedsRepair
|
|
5782
|
+
? 'auth_switch_validation_auto_deleted_quiet'
|
|
5783
|
+
: outcome.autoDeleted
|
|
5784
|
+
? 'auth_switch_validation_auto_deleted'
|
|
5785
|
+
: 'auth_switch_validation_failed';
|
|
5688
5786
|
return [
|
|
5689
|
-
t(locale,
|
|
5787
|
+
t(locale, validationKey, {
|
|
5690
5788
|
value: outcome.candidateName,
|
|
5691
5789
|
error: outcome.error ?? t(locale, 'unknown'),
|
|
5692
5790
|
}),
|
|
5693
5791
|
outcome.restoredPrevious ? t(locale, 'auth_switch_validation_reverted') : '',
|
|
5792
|
+
outcome.deleteRestarted ? t(locale, 'auth_delete_current_restarted') : '',
|
|
5793
|
+
outcome.autoDeleted ? formatCodexAuthPoolSummary(locale, this.store.getCodexAuthPoolStats()) : '',
|
|
5694
5794
|
].filter(Boolean);
|
|
5695
5795
|
}
|
|
5696
5796
|
async recoverCodexAuthCandidate(candidateName, options = { crossNode: true }) {
|
|
@@ -5709,8 +5809,23 @@ export class BridgeSessionCore {
|
|
|
5709
5809
|
return false;
|
|
5710
5810
|
}
|
|
5711
5811
|
}
|
|
5712
|
-
markCodexAuthCandidateNeedsRepair(candidateName) {
|
|
5812
|
+
async markCodexAuthCandidateNeedsRepair(candidateName) {
|
|
5813
|
+
if (this.config.authAutoDeleteNeedsRepair) {
|
|
5814
|
+
const candidate = (await this.listCodexAuthState()).candidates.find(entry => entry.name === candidateName) ?? null;
|
|
5815
|
+
if (!candidate) {
|
|
5816
|
+
this.store.deleteCodexAuthCandidate(candidateName);
|
|
5817
|
+
this.store.recordCodexAuthCandidateInvalidDelete(candidateName, AUTH_DELETE_REASON_NEEDS_REPAIR);
|
|
5818
|
+
return { deleted: true, restarted: false };
|
|
5819
|
+
}
|
|
5820
|
+
const restarted = await this.deleteCodexAuthCandidate(candidate, AUTH_DELETE_REASON_NEEDS_REPAIR);
|
|
5821
|
+
this.logger.warn('codex.auth_candidate_auto_deleted', {
|
|
5822
|
+
candidate: candidateName,
|
|
5823
|
+
runtimeId: this.authRuntimeId(),
|
|
5824
|
+
});
|
|
5825
|
+
return { deleted: true, restarted };
|
|
5826
|
+
}
|
|
5713
5827
|
this.store.setCodexAuthCandidateState(candidateName, 'needs_repair');
|
|
5828
|
+
return { deleted: false, restarted: false };
|
|
5714
5829
|
}
|
|
5715
5830
|
markCodexAuthCandidateActive(candidateName) {
|
|
5716
5831
|
this.store.setCodexAuthCandidateState(candidateName, 'active');
|
|
@@ -8078,7 +8193,7 @@ function formatFeaturesMessage(locale, features) {
|
|
|
8078
8193
|
}
|
|
8079
8194
|
return lines.join('\n');
|
|
8080
8195
|
}
|
|
8081
|
-
function formatConfigMessage(locale, result) {
|
|
8196
|
+
function formatConfigMessage(locale, result, appConfig, authPoolStats) {
|
|
8082
8197
|
const config = result.config && typeof result.config === 'object' ? result.config : {};
|
|
8083
8198
|
const layers = Array.isArray(result.layers) ? result.layers : [];
|
|
8084
8199
|
const keys = ['model', 'model_provider', 'approval_policy', 'sandbox_mode', 'web_search', 'service_tier', 'profile', 'review_model'];
|
|
@@ -8088,8 +8203,62 @@ function formatConfigMessage(locale, result) {
|
|
|
8088
8203
|
lines.push(`${key}: ${value === null || value === undefined ? '-' : formatConfigValue(value)}`);
|
|
8089
8204
|
}
|
|
8090
8205
|
lines.push(t(locale, 'config_layers', { count: layers.length }));
|
|
8206
|
+
lines.push('');
|
|
8207
|
+
lines.push(t(locale, 'config_foxclaw_title'));
|
|
8208
|
+
lines.push(t(locale, 'config_auth_auto_delete_needs_repair', {
|
|
8209
|
+
value: t(locale, appConfig.authAutoDeleteNeedsRepair ? 'yes' : 'no'),
|
|
8210
|
+
}));
|
|
8211
|
+
lines.push(`AUTH_AUTO_DELETE_NEEDS_REPAIR=${appConfig.authAutoDeleteNeedsRepair ? 'true' : 'false'}`);
|
|
8212
|
+
lines.push(formatCodexAuthPoolSummary(locale, authPoolStats));
|
|
8091
8213
|
return lines.join('\n');
|
|
8092
8214
|
}
|
|
8215
|
+
function formatCodexAuthPoolSummary(locale, stats) {
|
|
8216
|
+
return t(locale, 'auth_pool_summary', {
|
|
8217
|
+
total: stats.totalSeen,
|
|
8218
|
+
alive: stats.alive,
|
|
8219
|
+
deleted: stats.deletedInvalid,
|
|
8220
|
+
});
|
|
8221
|
+
}
|
|
8222
|
+
function isInvalidCodexAuthDeleteReason(reason) {
|
|
8223
|
+
return reason === AUTH_DELETE_REASON_NEEDS_REPAIR;
|
|
8224
|
+
}
|
|
8225
|
+
function configKeyboard(locale, appConfig) {
|
|
8226
|
+
const enabled = appConfig.authAutoDeleteNeedsRepair;
|
|
8227
|
+
return [[{
|
|
8228
|
+
text: t(locale, enabled ? 'button_config_auth_auto_delete_off' : 'button_config_auth_auto_delete_on'),
|
|
8229
|
+
callback_data: `config:auth_auto_delete:${enabled ? 'off' : 'on'}`,
|
|
8230
|
+
}]];
|
|
8231
|
+
}
|
|
8232
|
+
function parseConfigBooleanArg(value) {
|
|
8233
|
+
const normalized = value?.trim().toLowerCase();
|
|
8234
|
+
if (!normalized)
|
|
8235
|
+
return null;
|
|
8236
|
+
if (['1', 'true', 'yes', 'on', 'enable', 'enabled'].includes(normalized))
|
|
8237
|
+
return true;
|
|
8238
|
+
if (['0', 'false', 'no', 'off', 'disable', 'disabled'].includes(normalized))
|
|
8239
|
+
return false;
|
|
8240
|
+
return null;
|
|
8241
|
+
}
|
|
8242
|
+
async function writeEnvBoolean(envPath, key, enabled) {
|
|
8243
|
+
await fs.mkdir(path.dirname(envPath), { recursive: true });
|
|
8244
|
+
const nextLine = `${key}=${enabled ? 'true' : 'false'}`;
|
|
8245
|
+
let contents = '';
|
|
8246
|
+
try {
|
|
8247
|
+
contents = await fs.readFile(envPath, 'utf8');
|
|
8248
|
+
}
|
|
8249
|
+
catch {
|
|
8250
|
+
await fs.writeFile(envPath, `${nextLine}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
8251
|
+
return;
|
|
8252
|
+
}
|
|
8253
|
+
const pattern = new RegExp(`(^|\\n)[ \\t#]*${escapeRegExp(key)}\\s*=.*(?=\\r?\\n|$)`);
|
|
8254
|
+
const nextContents = pattern.test(contents)
|
|
8255
|
+
? contents.replace(pattern, (_match, prefix) => `${prefix}${nextLine}`)
|
|
8256
|
+
: `${contents}${contents.endsWith('\n') || contents.length === 0 ? '' : '\n'}${nextLine}\n`;
|
|
8257
|
+
await fs.writeFile(envPath, nextContents, { encoding: 'utf8', mode: 0o600 });
|
|
8258
|
+
}
|
|
8259
|
+
function escapeRegExp(value) {
|
|
8260
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
8261
|
+
}
|
|
8093
8262
|
function formatRequirementsMessage(locale, requirements) {
|
|
8094
8263
|
const lines = [t(locale, 'requirements_title')];
|
|
8095
8264
|
if (!requirements) {
|
|
@@ -9175,16 +9344,36 @@ function cloneAuthRetryContext(context) {
|
|
|
9175
9344
|
failedAuthTargets: new Set(context.failedAuthTargets),
|
|
9176
9345
|
};
|
|
9177
9346
|
}
|
|
9178
|
-
function
|
|
9179
|
-
|
|
9180
|
-
|
|
9347
|
+
function classifyCodexAuthRotationError(params) {
|
|
9348
|
+
const collected = collectCodexErrorText(params);
|
|
9349
|
+
if (isChatGptBackendAccessBlocked(collected)) {
|
|
9350
|
+
return null;
|
|
9181
9351
|
}
|
|
9182
9352
|
const code = stringOrNull(params?.error?.codexErrorInfo) ?? stringOrNull(params?.error?.code);
|
|
9183
|
-
if (code && /usageLimitExceeded|auth|unauthorized|forbidden|login/i.test(code)) {
|
|
9184
|
-
return true;
|
|
9185
|
-
}
|
|
9186
9353
|
const message = stringOrNull(params?.error?.message) ?? '';
|
|
9187
|
-
|
|
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);
|
|
9188
9377
|
}
|
|
9189
9378
|
function formatCodexNotificationError(params) {
|
|
9190
9379
|
const collected = collectCodexErrorText(params);
|
package/dist/i18n.d.ts
CHANGED
|
@@ -187,6 +187,8 @@ declare const MESSAGES: {
|
|
|
187
187
|
readonly auth_repair_reverted: "Restored previous auth. The candidate still needs repair.";
|
|
188
188
|
readonly auth_candidate_deleted: "Deleted auth candidate: {value}";
|
|
189
189
|
readonly auth_candidate_deleted_short: "Auth deleted";
|
|
190
|
+
readonly auth_candidate_auto_deleted: "Auto-deleted unrecoverable auth candidate: {value}";
|
|
191
|
+
readonly auth_pool_summary: "Auth pool: total seen {total}, alive {alive}, invalid-deleted {deleted}.";
|
|
190
192
|
readonly auth_delete_current_restarted: "The deleted candidate was current, so Codex app-server was restarted without it.";
|
|
191
193
|
readonly auth_no_candidates: "No auth candidates found. Expected files like auth.json_personal in the auth dir.";
|
|
192
194
|
readonly auth_choice_expired: "This auth list is no longer active";
|
|
@@ -195,6 +197,8 @@ declare const MESSAGES: {
|
|
|
195
197
|
readonly auth_switching: "Switching Codex auth: {from} -> {to}...";
|
|
196
198
|
readonly auth_switch_done: "Codex auth switched: {from} -> {to}.";
|
|
197
199
|
readonly auth_switch_validation_failed: "Selected auth failed validation: {error}. Marked {value} for login repair.";
|
|
200
|
+
readonly auth_switch_validation_auto_deleted: "Selected auth failed validation: {error}. Auto-deleted {value}.";
|
|
201
|
+
readonly auth_switch_validation_auto_deleted_quiet: "Selected auth failed validation and was auto-deleted.";
|
|
198
202
|
readonly auth_switch_validation_reverted: "Restored the previous auth after the failed switch.";
|
|
199
203
|
readonly auth_recovered_newer_candidate: "Recovered a newer same-account credential for {value} from another Codex home before reload.";
|
|
200
204
|
readonly auth_refresh_all_confirm_short: "Review refresh all risk first.";
|
|
@@ -211,11 +215,18 @@ declare const MESSAGES: {
|
|
|
211
215
|
readonly auth_refresh_all_skipped: "Skipped non-ChatGPT/invalid candidates: {value}";
|
|
212
216
|
readonly auth_refresh_all_failed: "Failed: {value}";
|
|
213
217
|
readonly auth_auto_switching: "Codex auth problem detected ({error}). Switching: {from} -> {to}...";
|
|
218
|
+
readonly auth_auto_switching_quiet: "Codex auth problem detected ({error}). Selecting another maintained candidate...";
|
|
214
219
|
readonly auth_auto_recovered_current: "Codex auth problem detected ({error}). Recovered a newer same-account credential for {value} and restarted Codex app-server.";
|
|
215
220
|
readonly auth_auto_done: "Auto-switched Codex auth: {from} -> {to}.";
|
|
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.";
|
|
216
226
|
readonly auth_auto_retrying: "Retrying the same request with the new auth...";
|
|
217
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.";
|
|
218
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.";
|
|
219
230
|
readonly auth_add_exists: "Auth candidate already exists: {value}";
|
|
220
231
|
readonly auth_add_preparing: "Preparing new Codex auth candidate {value}...";
|
|
221
232
|
readonly auth_add_started: "New auth login started for {value}.";
|
|
@@ -263,6 +274,8 @@ declare const MESSAGES: {
|
|
|
263
274
|
readonly button_auth_filter_all: "All";
|
|
264
275
|
readonly button_auth_filter_enabled: "Enabled";
|
|
265
276
|
readonly button_auth_filter_attention: "Attention";
|
|
277
|
+
readonly button_config_auth_auto_delete_on: "Auto-delete on";
|
|
278
|
+
readonly button_config_auth_auto_delete_off: "Auto-delete off";
|
|
266
279
|
readonly another_turn_running: "Another turn is already running. Use /interrupt, /takeover, /queue, or wait.";
|
|
267
280
|
readonly working: "Working...";
|
|
268
281
|
readonly usage_open: "Usage: /open <n>";
|
|
@@ -593,6 +606,11 @@ declare const MESSAGES: {
|
|
|
593
606
|
readonly features_empty: "No features found.";
|
|
594
607
|
readonly config_title: "Config summary";
|
|
595
608
|
readonly config_layers: "Config layers: {count}";
|
|
609
|
+
readonly config_foxclaw_title: "FoxClaw runtime config";
|
|
610
|
+
readonly config_auth_auto_delete_needs_repair: "Auto-delete unrecoverable auth candidates: {value}";
|
|
611
|
+
readonly config_auth_auto_delete_updated: "Auto-delete unrecoverable auth candidates set to: {value}";
|
|
612
|
+
readonly config_auth_auto_delete_usage: "Usage: /config auth_auto_delete <on|off>";
|
|
613
|
+
readonly config_env_update_failed: "Runtime setting changed, but updating {value} failed: {error}";
|
|
596
614
|
readonly requirements_title: "Config requirements";
|
|
597
615
|
readonly requirements_empty: "No config requirements are configured.";
|
|
598
616
|
readonly provider_title: "Model provider capabilities";
|
|
@@ -862,6 +880,8 @@ declare const MESSAGES: {
|
|
|
862
880
|
readonly auth_repair_reverted: "已恢复之前的 auth。该候选仍需要修复。";
|
|
863
881
|
readonly auth_candidate_deleted: "已删除 auth 候选:{value}";
|
|
864
882
|
readonly auth_candidate_deleted_short: "auth 已删除";
|
|
883
|
+
readonly auth_candidate_auto_deleted: "已自动剔除无法恢复的 auth 候选:{value}";
|
|
884
|
+
readonly auth_pool_summary: "auth 池:历史 {total},存活 {alive},因失效剔除 {deleted}。";
|
|
865
885
|
readonly auth_delete_current_restarted: "被删除的候选是当前 auth,已在删除后重启 Codex app-server。";
|
|
866
886
|
readonly auth_no_candidates: "没有找到 auth 候选文件。请在 auth 目录中放置类似 auth.json_personal 的文件。";
|
|
867
887
|
readonly auth_choice_expired: "这个 auth 列表已经不再有效";
|
|
@@ -870,6 +890,8 @@ declare const MESSAGES: {
|
|
|
870
890
|
readonly auth_switching: "正在切换 Codex auth:{from} -> {to}...";
|
|
871
891
|
readonly auth_switch_done: "Codex auth 已切换:{from} -> {to}。";
|
|
872
892
|
readonly auth_switch_validation_failed: "选中的 auth 验证失败:{error}。已将 {value} 标记为需要登录修复。";
|
|
893
|
+
readonly auth_switch_validation_auto_deleted: "选中的 auth 验证失败:{error}。已自动剔除 {value}。";
|
|
894
|
+
readonly auth_switch_validation_auto_deleted_quiet: "选中的 auth 验证失败,已自动剔除。";
|
|
873
895
|
readonly auth_switch_validation_reverted: "已在切换失败后恢复到之前的 auth。";
|
|
874
896
|
readonly auth_recovered_newer_candidate: "重载前已从其他 Codex home 恢复 {value} 的同账号较新凭据。";
|
|
875
897
|
readonly auth_refresh_all_confirm_short: "请先确认刷新全部风险。";
|
|
@@ -886,11 +908,18 @@ declare const MESSAGES: {
|
|
|
886
908
|
readonly auth_refresh_all_skipped: "已跳过非 ChatGPT/无效候选:{value}";
|
|
887
909
|
readonly auth_refresh_all_failed: "失败:{value}";
|
|
888
910
|
readonly auth_auto_switching: "检测到 Codex auth 问题({error}),正在切换:{from} -> {to}...";
|
|
911
|
+
readonly auth_auto_switching_quiet: "检测到 Codex auth 问题({error}),正在选择另一个维护中的候选...";
|
|
889
912
|
readonly auth_auto_recovered_current: "检测到 Codex auth 问题({error}),已为 {value} 恢复同账号较新凭据并重启 Codex app-server。";
|
|
890
913
|
readonly auth_auto_done: "已自动切换 Codex auth:{from} -> {to}。";
|
|
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 额度限制已用另一个维护中的候选处理。";
|
|
891
919
|
readonly auth_auto_retrying: "正在用新的 auth 重试同一条请求...";
|
|
892
920
|
readonly auth_auto_retry_thread_missing: "Auth 已切换,但原线程已经不可用({threadId})。已停止重试,避免创建重复 session。";
|
|
893
921
|
readonly auth_auto_no_candidate: "检测到 Codex auth 问题({error}),但没有可用的未失败候选 auth。";
|
|
922
|
+
readonly auth_quota_no_candidate: "检测到 Codex 额度限制({error}),但没有可用的未失败候选 auth。";
|
|
894
923
|
readonly auth_add_exists: "auth 候选已经存在:{value}";
|
|
895
924
|
readonly auth_add_preparing: "正在准备新的 Codex auth 候选 {value}...";
|
|
896
925
|
readonly auth_add_started: "{value} 的新 auth 登录已开始。";
|
|
@@ -938,6 +967,8 @@ declare const MESSAGES: {
|
|
|
938
967
|
readonly button_auth_filter_all: "全部";
|
|
939
968
|
readonly button_auth_filter_enabled: "已启用";
|
|
940
969
|
readonly button_auth_filter_attention: "需关注";
|
|
970
|
+
readonly button_config_auth_auto_delete_on: "开启自动剔除";
|
|
971
|
+
readonly button_config_auth_auto_delete_off: "关闭自动剔除";
|
|
941
972
|
readonly another_turn_running: "已经有一个回复在进行中。请先等待,或使用 /interrupt、/takeover、/queue。";
|
|
942
973
|
readonly working: "处理中...";
|
|
943
974
|
readonly usage_open: "用法:/open <编号>";
|
|
@@ -1268,6 +1299,11 @@ declare const MESSAGES: {
|
|
|
1268
1299
|
readonly features_empty: "没有找到功能开关。";
|
|
1269
1300
|
readonly config_title: "配置摘要";
|
|
1270
1301
|
readonly config_layers: "配置层数:{count}";
|
|
1302
|
+
readonly config_foxclaw_title: "FoxClaw 运行时配置";
|
|
1303
|
+
readonly config_auth_auto_delete_needs_repair: "自动剔除无法恢复的 auth 候选:{value}";
|
|
1304
|
+
readonly config_auth_auto_delete_updated: "自动剔除无法恢复的 auth 候选已设置为:{value}";
|
|
1305
|
+
readonly config_auth_auto_delete_usage: "用法:/config auth_auto_delete <on|off>";
|
|
1306
|
+
readonly config_env_update_failed: "运行时设置已改变,但更新 {value} 失败:{error}";
|
|
1271
1307
|
readonly requirements_title: "配置要求";
|
|
1272
1308
|
readonly requirements_empty: "当前没有配置要求。";
|
|
1273
1309
|
readonly provider_title: "模型供应商能力";
|