@foxden-app/foxclaw 0.5.19 → 0.5.21

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.
@@ -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;
@@ -119,6 +120,8 @@ export class BridgeSessionCore {
119
120
  pendingAuthRotation = null;
120
121
  authRotationInProgress = false;
121
122
  authRefreshAllInProgress = false;
123
+ externalAuthValidationInProgress = false;
124
+ turnStartInProgress = 0;
122
125
  authRotationFailedTargets = new Set();
123
126
  localUsageCache = null;
124
127
  localUsageCacheLoaded = false;
@@ -245,6 +248,8 @@ export class BridgeSessionCore {
245
248
  this.commandUsageSequence = 0;
246
249
  this.pendingAuthRotation = null;
247
250
  this.authRefreshAllInProgress = false;
251
+ this.externalAuthValidationInProgress = false;
252
+ this.turnStartInProgress = 0;
248
253
  this.clearObservedThreadWatchers();
249
254
  this.releaseActiveTurnsForBridgeShutdown();
250
255
  this.bot.stop();
@@ -311,6 +316,10 @@ export class BridgeSessionCore {
311
316
  await this.handleActiveTurnInboundMessage(event, locale, event.text.trim());
312
317
  return;
313
318
  }
319
+ if (this.externalAuthValidationInProgress) {
320
+ await this.sendMessage(scopeId, t(locale, 'auth_sync_validation_busy'));
321
+ return;
322
+ }
314
323
  await this.startBoundTurnFromEvent(event, locale, event.text.trim());
315
324
  return;
316
325
  }
@@ -365,6 +374,10 @@ export class BridgeSessionCore {
365
374
  await this.handleActiveTurnInboundMessage(event, locale, decision.text);
366
375
  return;
367
376
  }
377
+ if (this.externalAuthValidationInProgress) {
378
+ await this.sendMessage(scopeId, t(locale, 'auth_sync_validation_busy'));
379
+ return;
380
+ }
368
381
  await this.startBoundTurnFromEvent(event, locale, decision.text);
369
382
  }
370
383
  async handleCommand(event, locale, name, args) {
@@ -420,6 +433,7 @@ export class BridgeSessionCore {
420
433
  t(locale, 'status_pending_user_inputs', { value: this.store.countPendingUserInputs() }),
421
434
  t(locale, 'status_queued_turns', { value: this.store.countQueuedTurnInputs(scopeId) }),
422
435
  t(locale, 'status_active_turns', { value: this.activeTurns.size }),
436
+ formatCodexAuthPoolSummary(locale, this.store.getCodexAuthPoolStats()),
423
437
  ];
424
438
  if (serviceStatus) {
425
439
  lines.push('', t(locale, 'status_runtime_overview'));
@@ -743,7 +757,7 @@ export class BridgeSessionCore {
743
757
  return;
744
758
  }
745
759
  case 'config': {
746
- await this.handleConfigCommand(scopeId, locale);
760
+ await this.handleConfigCommand(scopeId, locale, args);
747
761
  return;
748
762
  }
749
763
  case 'requirements': {
@@ -1041,6 +1055,11 @@ export class BridgeSessionCore {
1041
1055
  await this.handleSetupCallback(event, setupMatch[1], setupMatch[2], locale);
1042
1056
  return;
1043
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
+ }
1044
1063
  const settingsMatch = /^settings:(model|effort|access):(.+)$/.exec(event.data);
1045
1064
  if (settingsMatch) {
1046
1065
  await this.handleSettingsCallback(event, settingsMatch[1], settingsMatch[2], locale);
@@ -2919,53 +2938,75 @@ export class BridgeSessionCore {
2919
2938
  async getCurrentAuthLabel() {
2920
2939
  return (await this.listCodexAuthState()).currentLabel;
2921
2940
  }
2941
+ async handleExternalCodexAuthCandidateDeleted(candidateName, reason = null) {
2942
+ this.store.deleteCodexAuthCandidate(candidateName);
2943
+ if (isInvalidCodexAuthDeleteReason(reason)) {
2944
+ this.store.recordCodexAuthCandidateInvalidDelete(candidateName, reason);
2945
+ }
2946
+ else {
2947
+ this.store.recordCodexAuthCandidateRemoved(candidateName, reason);
2948
+ }
2949
+ this.authRotationFailedTargets.delete(path.join(this.resolveAuthDir(), candidateName));
2950
+ this.pendingTurnErrors.clear();
2951
+ this.attachedThreads.clear();
2952
+ await this.app.restart();
2953
+ }
2922
2954
  async validateExternalCodexAuthCandidate(candidateName, rawAuth, expectedAccountId) {
2923
- if (!this.isIdleForServiceUpdate()) {
2955
+ if (this.externalAuthValidationInProgress) {
2924
2956
  return { ok: false, reason: 'runtime is not idle' };
2925
2957
  }
2926
- const metadata = parseChatGptAuthMetadata(rawAuth);
2927
- if (!metadata || metadata.accountId !== expectedAccountId) {
2928
- return { ok: false, reason: 'remote auth account id mismatch' };
2929
- }
2930
- const state = await this.listCodexAuthState();
2931
- const existing = state.candidates.find(candidate => candidate.name === candidateName) ?? null;
2932
- if (existing) {
2933
- const existingMetadata = await readChatGptAuthMetadata(existing.path);
2934
- if (existingMetadata && existingMetadata.accountId !== expectedAccountId) {
2935
- return { ok: false, reason: 'same candidate belongs to a different account' };
2936
- }
2958
+ if (!this.isIdleForServiceUpdate()) {
2959
+ return { ok: false, reason: 'runtime is not idle' };
2937
2960
  }
2938
- const authStat = await fs.lstat(state.authPath).catch(() => null);
2939
- const originalRegularAuth = authStat?.isFile()
2940
- ? await fs.readFile(state.authPath, 'utf8').catch(() => null)
2941
- : null;
2942
- const tempPath = path.join(state.authDir, `.auth-sync-validate-${process.pid}-${Date.now()}.json`);
2961
+ this.externalAuthValidationInProgress = true;
2943
2962
  try {
2944
- await fs.writeFile(tempPath, rawAuth, { encoding: 'utf8', mode: 0o600 });
2945
- await pointCodexAuthAtTarget(state.authDir, state.authPath, tempPath);
2946
- this.pendingTurnErrors.clear();
2947
- this.attachedThreads.clear();
2948
- await this.app.restart();
2949
- const account = await this.app.readAccount(false);
2950
- const rateLimits = await this.app.readAccountRateLimits();
2951
- if (!account || !rateLimits || !selectCodexRateLimitSnapshot(rateLimits)) {
2952
- return { ok: false, reason: 'Codex did not validate ChatGPT usage for remote auth' };
2963
+ const metadata = parseChatGptAuthMetadata(rawAuth);
2964
+ if (!metadata || metadata.accountId !== expectedAccountId) {
2965
+ return { ok: false, reason: 'remote auth account id mismatch' };
2966
+ }
2967
+ const state = await this.listCodexAuthState();
2968
+ const existing = state.candidates.find(candidate => candidate.name === candidateName) ?? null;
2969
+ if (existing) {
2970
+ const existingMetadata = await readChatGptAuthMetadata(existing.path);
2971
+ if (existingMetadata && existingMetadata.accountId !== expectedAccountId) {
2972
+ return { ok: false, reason: 'same candidate belongs to a different account' };
2973
+ }
2974
+ }
2975
+ const authStat = await fs.lstat(state.authPath).catch(() => null);
2976
+ const originalRegularAuth = authStat?.isFile()
2977
+ ? await fs.readFile(state.authPath, 'utf8').catch(() => null)
2978
+ : null;
2979
+ const tempPath = path.join(state.authDir, `.auth-sync-validate-${process.pid}-${Date.now()}.json`);
2980
+ try {
2981
+ await fs.writeFile(tempPath, rawAuth, { encoding: 'utf8', mode: 0o600 });
2982
+ await pointCodexAuthAtTarget(state.authDir, state.authPath, tempPath);
2983
+ this.pendingTurnErrors.clear();
2984
+ this.attachedThreads.clear();
2985
+ await this.app.restart();
2986
+ const account = await this.app.readAccount(false);
2987
+ const rateLimits = await this.app.readAccountRateLimits();
2988
+ if (!account || !rateLimits || !selectCodexRateLimitSnapshot(rateLimits)) {
2989
+ return { ok: false, reason: 'Codex did not validate ChatGPT usage for remote auth' };
2990
+ }
2991
+ return { ok: true };
2992
+ }
2993
+ catch (error) {
2994
+ return { ok: false, reason: formatUserError(error) };
2995
+ }
2996
+ finally {
2997
+ await restoreCodexAuthTarget(state.authDir, state.authPath, state.currentTargetPath, originalRegularAuth).catch((error) => {
2998
+ this.logger.warn('codex.auth_sync_restore_failed', { error: toErrorMeta(error) });
2999
+ });
3000
+ await fs.rm(tempPath, { force: true }).catch(() => undefined);
3001
+ this.pendingTurnErrors.clear();
3002
+ this.attachedThreads.clear();
3003
+ await this.app.restart().catch((error) => {
3004
+ this.logger.warn('codex.auth_sync_restart_restore_failed', { error: toErrorMeta(error) });
3005
+ });
2953
3006
  }
2954
- return { ok: true };
2955
- }
2956
- catch (error) {
2957
- return { ok: false, reason: formatUserError(error) };
2958
3007
  }
2959
3008
  finally {
2960
- await restoreCodexAuthTarget(state.authDir, state.authPath, state.currentTargetPath, originalRegularAuth).catch((error) => {
2961
- this.logger.warn('codex.auth_sync_restore_failed', { error: toErrorMeta(error) });
2962
- });
2963
- await fs.rm(tempPath, { force: true }).catch(() => undefined);
2964
- this.pendingTurnErrors.clear();
2965
- this.attachedThreads.clear();
2966
- await this.app.restart().catch((error) => {
2967
- this.logger.warn('codex.auth_sync_restart_restore_failed', { error: toErrorMeta(error) });
2968
- });
3009
+ this.externalAuthValidationInProgress = false;
2969
3010
  }
2970
3011
  }
2971
3012
  isIdleForServiceUpdate() {
@@ -2975,7 +3016,9 @@ export class BridgeSessionCore {
2975
3016
  && this.pendingMcpElicitations.size === 0
2976
3017
  && this.pendingLoginsByScope.size === 0
2977
3018
  && !this.authRotationInProgress
2978
- && !this.authRefreshAllInProgress;
3019
+ && !this.authRefreshAllInProgress
3020
+ && !this.externalAuthValidationInProgress
3021
+ && this.turnStartInProgress === 0;
2979
3022
  }
2980
3023
  hasLocalBlockingActivity() {
2981
3024
  return !this.isIdleForServiceUpdate();
@@ -4738,13 +4781,13 @@ export class BridgeSessionCore {
4738
4781
  throw error;
4739
4782
  }
4740
4783
  }
4741
- async deleteCodexAuthCandidate(candidate) {
4784
+ async deleteCodexAuthCandidate(candidate, reason = null) {
4742
4785
  const wasCurrent = candidate.isCurrent;
4743
4786
  const authDir = this.resolveAuthDir();
4744
4787
  const authPath = path.join(authDir, 'auth.json');
4745
4788
  let deletedByCoordinator = false;
4746
4789
  try {
4747
- await this.coordinator?.authCandidateDeleted?.(this.authRuntimeId(), candidate.name);
4790
+ await this.coordinator?.authCandidateDeleted?.(this.authRuntimeId(), candidate.name, reason);
4748
4791
  deletedByCoordinator = Boolean(this.coordinator?.authCandidateDeleted);
4749
4792
  }
4750
4793
  catch (error) {
@@ -4761,6 +4804,12 @@ export class BridgeSessionCore {
4761
4804
  }
4762
4805
  }
4763
4806
  this.store.deleteCodexAuthCandidate(candidate.name);
4807
+ if (isInvalidCodexAuthDeleteReason(reason)) {
4808
+ this.store.recordCodexAuthCandidateInvalidDelete(candidate.name, reason);
4809
+ }
4810
+ else {
4811
+ this.store.recordCodexAuthCandidateRemoved(candidate.name, reason);
4812
+ }
4764
4813
  this.authRotationFailedTargets.delete(candidate.path);
4765
4814
  const snapshots = await this.readCodexAuthQuotaSnapshots();
4766
4815
  if (Object.prototype.hasOwnProperty.call(snapshots, candidate.name)) {
@@ -5069,10 +5118,58 @@ export class BridgeSessionCore {
5069
5118
  const features = await this.app.listExperimentalFeatures();
5070
5119
  await this.sendMessage(scopeId, formatFeaturesMessage(locale, features));
5071
5120
  }
5072
- async handleConfigCommand(scopeId, locale) {
5121
+ async handleConfigCommand(scopeId, locale, args = []) {
5122
+ const action = args[0]?.toLowerCase() ?? '';
5123
+ if (['auth_auto_delete', 'auth-auto-delete', 'auto_delete_needs_repair', 'auto-delete-needs-repair'].includes(action)) {
5124
+ const enabled = parseConfigBooleanArg(args[1]);
5125
+ if (enabled === null) {
5126
+ await this.sendMessage(scopeId, t(locale, 'config_auth_auto_delete_usage'));
5127
+ return;
5128
+ }
5129
+ const update = await this.setAuthAutoDeleteNeedsRepair(enabled);
5130
+ const binding = this.store.getBinding(scopeId);
5131
+ const result = await this.app.readConfig(binding?.cwd ?? this.config.defaultCwd, true);
5132
+ await this.sendMessage(scopeId, `${this.formatConfigToggleUpdate(locale, update)}\n\n${formatConfigMessage(locale, result, this.config, this.store.getCodexAuthPoolStats())}`, configKeyboard(locale, this.config));
5133
+ return;
5134
+ }
5073
5135
  const binding = this.store.getBinding(scopeId);
5074
5136
  const result = await this.app.readConfig(binding?.cwd ?? this.config.defaultCwd, true);
5075
- await this.sendMessage(scopeId, formatConfigMessage(locale, result));
5137
+ await this.sendMessage(scopeId, formatConfigMessage(locale, result, this.config, this.store.getCodexAuthPoolStats()), configKeyboard(locale, this.config));
5138
+ }
5139
+ async handleConfigToggleCallback(event, enabled, locale) {
5140
+ const update = await this.setAuthAutoDeleteNeedsRepair(enabled);
5141
+ await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'decision_recorded'));
5142
+ const binding = this.store.getBinding(event.scopeId);
5143
+ const result = await this.app.readConfig(binding?.cwd ?? this.config.defaultCwd, true);
5144
+ const message = `${this.formatConfigToggleUpdate(locale, update)}\n\n${formatConfigMessage(locale, result, this.config, this.store.getCodexAuthPoolStats())}`;
5145
+ if (event.messageId !== null) {
5146
+ await this.editMessage(event.scopeId, event.messageId, message, configKeyboard(locale, this.config));
5147
+ }
5148
+ else {
5149
+ await this.sendMessage(event.scopeId, message, configKeyboard(locale, this.config));
5150
+ }
5151
+ }
5152
+ async setAuthAutoDeleteNeedsRepair(enabled) {
5153
+ this.config.authAutoDeleteNeedsRepair = enabled;
5154
+ const envPath = this.config.envPath;
5155
+ if (!envPath) {
5156
+ return { enabled, envPath: null, envUpdated: false, envError: null };
5157
+ }
5158
+ try {
5159
+ await writeEnvBoolean(envPath, 'AUTH_AUTO_DELETE_NEEDS_REPAIR', enabled);
5160
+ return { enabled, envPath, envUpdated: true, envError: null };
5161
+ }
5162
+ catch (error) {
5163
+ this.logger.warn('config.env_update_failed', { key: 'AUTH_AUTO_DELETE_NEEDS_REPAIR', envPath, error: toErrorMeta(error) });
5164
+ return { enabled, envPath, envUpdated: false, envError: formatUserError(error) };
5165
+ }
5166
+ }
5167
+ formatConfigToggleUpdate(locale, update) {
5168
+ const lines = [t(locale, 'config_auth_auto_delete_updated', { value: t(locale, update.enabled ? 'yes' : 'no') })];
5169
+ if (update.envError) {
5170
+ lines.push(t(locale, 'config_env_update_failed', { value: update.envPath ?? t(locale, 'unknown'), error: update.envError }));
5171
+ }
5172
+ return lines.join('\n');
5076
5173
  }
5077
5174
  async handleRequirementsCommand(scopeId, locale) {
5078
5175
  const requirements = await this.app.readConfigRequirements();
@@ -5424,7 +5521,10 @@ export class BridgeSessionCore {
5424
5521
  }
5425
5522
  return false;
5426
5523
  }
5427
- this.markCodexAuthCandidateNeedsRepair(current.name);
5524
+ const disposition = await this.markCodexAuthCandidateNeedsRepair(current.name);
5525
+ if (disposition.deleted) {
5526
+ await this.sendMessage(rotation.scopeId, formatCodexAuthPoolSummary(locale, this.store.getCodexAuthPoolStats()));
5527
+ }
5428
5528
  }
5429
5529
  const selection = await this.selectNextCodexAuthCandidate(failedTargets);
5430
5530
  if (!selection) {
@@ -5434,10 +5534,12 @@ export class BridgeSessionCore {
5434
5534
  return false;
5435
5535
  }
5436
5536
  const { candidate, fromLabel, toLabel } = selection;
5437
- await this.sendMessage(rotation.scopeId, t(locale, 'auth_auto_switching', {
5438
- ...this.codexAuthSwitchParams(locale, fromLabel, toLabel),
5439
- error: formatShortStatusError(rotation.reason),
5440
- }));
5537
+ 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', {
5540
+ ...this.codexAuthSwitchParams(locale, fromLabel, toLabel),
5541
+ error: formatShortStatusError(rotation.reason),
5542
+ }));
5441
5543
  const outcome = await this.switchCodexAuthAndRestart(rotation.scopeId, locale, candidate, true);
5442
5544
  if (!outcome.ok) {
5443
5545
  return false;
@@ -5525,6 +5627,7 @@ export class BridgeSessionCore {
5525
5627
  }
5526
5628
  async listCodexAuthState() {
5527
5629
  const state = await listCodexAuthState(this.store.listDisabledCodexAuthCandidateNames(this.authRuntimeId()), this.store.listCodexAuthCandidateStates(this.authRuntimeId()), this.resolveAuthDir());
5630
+ this.store.recordCodexAuthPoolInventory(state.candidates.map(candidate => candidate.name));
5528
5631
  const snapshots = await this.readCodexAuthQuotaSnapshots();
5529
5632
  const candidateQuotaIdentities = await this.readCodexAuthCandidateQuotaIdentities(state.candidates);
5530
5633
  state.candidates.forEach((candidate) => {
@@ -5571,7 +5674,6 @@ export class BridgeSessionCore {
5571
5674
  await this.app.restart();
5572
5675
  const validation = await this.validateCurrentCodexAuthCandidate(candidate);
5573
5676
  if (!validation.ok) {
5574
- this.markCodexAuthCandidateNeedsRepair(candidate.name);
5575
5677
  let restoredPrevious = false;
5576
5678
  try {
5577
5679
  await restoreCodexAuthTarget(initialState.authDir, initialState.authPath, initialState.currentTargetPath, originalRegularAuth);
@@ -5586,6 +5688,7 @@ export class BridgeSessionCore {
5586
5688
  error: toErrorMeta(error),
5587
5689
  });
5588
5690
  }
5691
+ const repairDisposition = await this.markCodexAuthCandidateNeedsRepair(candidate.name);
5589
5692
  const outcome = {
5590
5693
  ...result,
5591
5694
  ok: false,
@@ -5593,6 +5696,8 @@ export class BridgeSessionCore {
5593
5696
  recovered,
5594
5697
  error: validation.error,
5595
5698
  restoredPrevious,
5699
+ autoDeleted: repairDisposition.deleted,
5700
+ deleteRestarted: repairDisposition.restarted,
5596
5701
  };
5597
5702
  if (sendResult) {
5598
5703
  const lines = this.formatAuthSwitchValidationLines(locale, outcome);
@@ -5609,11 +5714,15 @@ export class BridgeSessionCore {
5609
5714
  recovered,
5610
5715
  error: null,
5611
5716
  restoredPrevious: false,
5717
+ autoDeleted: false,
5718
+ deleteRestarted: false,
5612
5719
  };
5613
5720
  if (!sendResult) {
5614
5721
  return outcome;
5615
5722
  }
5616
- const lines = [t(locale, automatic ? 'auth_auto_done' : 'auth_switch_done', this.codexAuthSwitchParams(locale, result.fromLabel, result.toLabel))];
5723
+ 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))];
5617
5726
  if (recovered) {
5618
5727
  lines.push(t(locale, 'auth_recovered_newer_candidate', { value: candidate.name }));
5619
5728
  }
@@ -5662,12 +5771,19 @@ export class BridgeSessionCore {
5662
5771
  if (outcome.ok) {
5663
5772
  return [];
5664
5773
  }
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';
5665
5779
  return [
5666
- t(locale, 'auth_switch_validation_failed', {
5780
+ t(locale, validationKey, {
5667
5781
  value: outcome.candidateName,
5668
5782
  error: outcome.error ?? t(locale, 'unknown'),
5669
5783
  }),
5670
5784
  outcome.restoredPrevious ? t(locale, 'auth_switch_validation_reverted') : '',
5785
+ outcome.deleteRestarted ? t(locale, 'auth_delete_current_restarted') : '',
5786
+ outcome.autoDeleted ? formatCodexAuthPoolSummary(locale, this.store.getCodexAuthPoolStats()) : '',
5671
5787
  ].filter(Boolean);
5672
5788
  }
5673
5789
  async recoverCodexAuthCandidate(candidateName, options = { crossNode: true }) {
@@ -5686,8 +5802,23 @@ export class BridgeSessionCore {
5686
5802
  return false;
5687
5803
  }
5688
5804
  }
5689
- markCodexAuthCandidateNeedsRepair(candidateName) {
5805
+ async markCodexAuthCandidateNeedsRepair(candidateName) {
5806
+ if (this.config.authAutoDeleteNeedsRepair) {
5807
+ const candidate = (await this.listCodexAuthState()).candidates.find(entry => entry.name === candidateName) ?? null;
5808
+ if (!candidate) {
5809
+ this.store.deleteCodexAuthCandidate(candidateName);
5810
+ this.store.recordCodexAuthCandidateInvalidDelete(candidateName, AUTH_DELETE_REASON_NEEDS_REPAIR);
5811
+ return { deleted: true, restarted: false };
5812
+ }
5813
+ const restarted = await this.deleteCodexAuthCandidate(candidate, AUTH_DELETE_REASON_NEEDS_REPAIR);
5814
+ this.logger.warn('codex.auth_candidate_auto_deleted', {
5815
+ candidate: candidateName,
5816
+ runtimeId: this.authRuntimeId(),
5817
+ });
5818
+ return { deleted: true, restarted };
5819
+ }
5690
5820
  this.store.setCodexAuthCandidateState(candidateName, 'needs_repair');
5821
+ return { deleted: false, restarted: false };
5691
5822
  }
5692
5823
  markCodexAuthCandidateActive(candidateName) {
5693
5824
  this.store.setCodexAuthCandidateState(candidateName, 'active');
@@ -6913,36 +7044,46 @@ export class BridgeSessionCore {
6913
7044
  }
6914
7045
  async startBoundTurnFromEvent(event, locale, text) {
6915
7046
  const scopeId = event.scopeId;
6916
- this.clearPlanImplementationPromptsForScope(scopeId);
6917
- await this.stopWatchingScopeThread(scopeId);
6918
- const existingBinding = this.store.getBinding(scopeId);
6919
- const binding = existingBinding
6920
- ? await this.ensureThreadReady(scopeId, existingBinding)
6921
- : await this.createBinding(scopeId, null);
6922
- await this.sendTyping(scopeId);
6923
- const previewMessageId = 0;
7047
+ if (this.externalAuthValidationInProgress) {
7048
+ await this.sendMessage(scopeId, t(locale, 'auth_sync_validation_busy'));
7049
+ return;
7050
+ }
7051
+ this.turnStartInProgress += 1;
6924
7052
  try {
6925
- const input = await this.buildTurnInput(binding, { ...event, text }, locale);
6926
- const turnState = await this.startTurnWithRecovery(scopeId, binding, input);
6927
- if (turnState.collaborationMode === 'plan') {
6928
- this.store.setChatCollaborationMode(scopeId, DEFAULT_COLLABORATION_MODE);
7053
+ this.clearPlanImplementationPromptsForScope(scopeId);
7054
+ await this.stopWatchingScopeThread(scopeId);
7055
+ const existingBinding = this.store.getBinding(scopeId);
7056
+ const binding = existingBinding
7057
+ ? await this.ensureThreadReady(scopeId, existingBinding)
7058
+ : await this.createBinding(scopeId, null);
7059
+ await this.sendTyping(scopeId);
7060
+ const previewMessageId = 0;
7061
+ try {
7062
+ const input = await this.buildTurnInput(binding, { ...event, text }, locale);
7063
+ const turnState = await this.startTurnWithRecovery(scopeId, binding, input);
7064
+ if (turnState.collaborationMode === 'plan') {
7065
+ this.store.setChatCollaborationMode(scopeId, DEFAULT_COLLABORATION_MODE);
7066
+ }
7067
+ await this.registerActiveTurn(scopeId, event.chatId, event.chatType, event.topicId, turnState.threadId, turnState.turnId, previewMessageId, {
7068
+ input,
7069
+ threadId: turnState.threadId,
7070
+ cwd: this.store.getBinding(scopeId)?.cwd ?? binding.cwd ?? this.config.defaultCwd,
7071
+ chatId: event.chatId,
7072
+ chatType: event.chatType,
7073
+ topicId: event.topicId,
7074
+ collaborationMode: turnState.collaborationMode,
7075
+ failedAuthTargets: new Set(),
7076
+ }, turnState.collaborationMode);
6929
7077
  }
6930
- await this.registerActiveTurn(scopeId, event.chatId, event.chatType, event.topicId, turnState.threadId, turnState.turnId, previewMessageId, {
6931
- input,
6932
- threadId: turnState.threadId,
6933
- cwd: this.store.getBinding(scopeId)?.cwd ?? binding.cwd ?? this.config.defaultCwd,
6934
- chatId: event.chatId,
6935
- chatType: event.chatType,
6936
- topicId: event.topicId,
6937
- collaborationMode: turnState.collaborationMode,
6938
- failedAuthTargets: new Set(),
6939
- }, turnState.collaborationMode);
6940
- }
6941
- catch (error) {
6942
- if (previewMessageId > 0) {
6943
- await this.cleanupTransientPreview(scopeId, previewMessageId);
7078
+ catch (error) {
7079
+ if (previewMessageId > 0) {
7080
+ await this.cleanupTransientPreview(scopeId, previewMessageId);
7081
+ }
7082
+ throw error;
6944
7083
  }
6945
- throw error;
7084
+ }
7085
+ finally {
7086
+ this.turnStartInProgress -= 1;
6946
7087
  }
6947
7088
  }
6948
7089
  async requestInterrupt(active) {
@@ -8045,7 +8186,7 @@ function formatFeaturesMessage(locale, features) {
8045
8186
  }
8046
8187
  return lines.join('\n');
8047
8188
  }
8048
- function formatConfigMessage(locale, result) {
8189
+ function formatConfigMessage(locale, result, appConfig, authPoolStats) {
8049
8190
  const config = result.config && typeof result.config === 'object' ? result.config : {};
8050
8191
  const layers = Array.isArray(result.layers) ? result.layers : [];
8051
8192
  const keys = ['model', 'model_provider', 'approval_policy', 'sandbox_mode', 'web_search', 'service_tier', 'profile', 'review_model'];
@@ -8055,8 +8196,62 @@ function formatConfigMessage(locale, result) {
8055
8196
  lines.push(`${key}: ${value === null || value === undefined ? '-' : formatConfigValue(value)}`);
8056
8197
  }
8057
8198
  lines.push(t(locale, 'config_layers', { count: layers.length }));
8199
+ lines.push('');
8200
+ lines.push(t(locale, 'config_foxclaw_title'));
8201
+ lines.push(t(locale, 'config_auth_auto_delete_needs_repair', {
8202
+ value: t(locale, appConfig.authAutoDeleteNeedsRepair ? 'yes' : 'no'),
8203
+ }));
8204
+ lines.push(`AUTH_AUTO_DELETE_NEEDS_REPAIR=${appConfig.authAutoDeleteNeedsRepair ? 'true' : 'false'}`);
8205
+ lines.push(formatCodexAuthPoolSummary(locale, authPoolStats));
8058
8206
  return lines.join('\n');
8059
8207
  }
8208
+ function formatCodexAuthPoolSummary(locale, stats) {
8209
+ return t(locale, 'auth_pool_summary', {
8210
+ total: stats.totalSeen,
8211
+ alive: stats.alive,
8212
+ deleted: stats.deletedInvalid,
8213
+ });
8214
+ }
8215
+ function isInvalidCodexAuthDeleteReason(reason) {
8216
+ return reason === AUTH_DELETE_REASON_NEEDS_REPAIR;
8217
+ }
8218
+ function configKeyboard(locale, appConfig) {
8219
+ const enabled = appConfig.authAutoDeleteNeedsRepair;
8220
+ return [[{
8221
+ text: t(locale, enabled ? 'button_config_auth_auto_delete_off' : 'button_config_auth_auto_delete_on'),
8222
+ callback_data: `config:auth_auto_delete:${enabled ? 'off' : 'on'}`,
8223
+ }]];
8224
+ }
8225
+ function parseConfigBooleanArg(value) {
8226
+ const normalized = value?.trim().toLowerCase();
8227
+ if (!normalized)
8228
+ return null;
8229
+ if (['1', 'true', 'yes', 'on', 'enable', 'enabled'].includes(normalized))
8230
+ return true;
8231
+ if (['0', 'false', 'no', 'off', 'disable', 'disabled'].includes(normalized))
8232
+ return false;
8233
+ return null;
8234
+ }
8235
+ async function writeEnvBoolean(envPath, key, enabled) {
8236
+ await fs.mkdir(path.dirname(envPath), { recursive: true });
8237
+ const nextLine = `${key}=${enabled ? 'true' : 'false'}`;
8238
+ let contents = '';
8239
+ try {
8240
+ contents = await fs.readFile(envPath, 'utf8');
8241
+ }
8242
+ catch {
8243
+ await fs.writeFile(envPath, `${nextLine}\n`, { encoding: 'utf8', mode: 0o600 });
8244
+ return;
8245
+ }
8246
+ const pattern = new RegExp(`(^|\\n)[ \\t#]*${escapeRegExp(key)}\\s*=.*(?=\\r?\\n|$)`);
8247
+ const nextContents = pattern.test(contents)
8248
+ ? contents.replace(pattern, (_match, prefix) => `${prefix}${nextLine}`)
8249
+ : `${contents}${contents.endsWith('\n') || contents.length === 0 ? '' : '\n'}${nextLine}\n`;
8250
+ await fs.writeFile(envPath, nextContents, { encoding: 'utf8', mode: 0o600 });
8251
+ }
8252
+ function escapeRegExp(value) {
8253
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
8254
+ }
8060
8255
  function formatRequirementsMessage(locale, requirements) {
8061
8256
  const lines = [t(locale, 'requirements_title')];
8062
8257
  if (!requirements) {