@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.
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,8 +215,10 @@ 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.";
216
222
  readonly auth_auto_retrying: "Retrying the same request with the new auth...";
217
223
  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
224
  readonly auth_auto_no_candidate: "Codex auth problem detected ({error}), but no unused auth candidate is available.";
@@ -225,6 +231,7 @@ declare const MESSAGES: {
225
231
  readonly auth_add_reverted: "Restored previous auth.";
226
232
  readonly auth_add_missing_file: "Login completed, but the new auth file was not created: {value}";
227
233
  readonly auth_sync_disabled: "Cross-node auth sync is disabled.";
234
+ readonly auth_sync_validation_busy: "Auth sync is validating refreshed credentials and temporarily restarting Codex app-server. Wait a moment, then send this message again.";
228
235
  readonly auth_sync_test_sent: "Auth sync test complete: sent {sent}, replies {replied}.";
229
236
  readonly auth_sync_test_missing: "Missing replies: {value}";
230
237
  readonly auth_sync_push_blocked_active: "Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.";
@@ -262,6 +269,8 @@ declare const MESSAGES: {
262
269
  readonly button_auth_filter_all: "All";
263
270
  readonly button_auth_filter_enabled: "Enabled";
264
271
  readonly button_auth_filter_attention: "Attention";
272
+ readonly button_config_auth_auto_delete_on: "Auto-delete on";
273
+ readonly button_config_auth_auto_delete_off: "Auto-delete off";
265
274
  readonly another_turn_running: "Another turn is already running. Use /interrupt, /takeover, /queue, or wait.";
266
275
  readonly working: "Working...";
267
276
  readonly usage_open: "Usage: /open <n>";
@@ -592,6 +601,11 @@ declare const MESSAGES: {
592
601
  readonly features_empty: "No features found.";
593
602
  readonly config_title: "Config summary";
594
603
  readonly config_layers: "Config layers: {count}";
604
+ readonly config_foxclaw_title: "FoxClaw runtime config";
605
+ readonly config_auth_auto_delete_needs_repair: "Auto-delete unrecoverable auth candidates: {value}";
606
+ readonly config_auth_auto_delete_updated: "Auto-delete unrecoverable auth candidates set to: {value}";
607
+ readonly config_auth_auto_delete_usage: "Usage: /config auth_auto_delete <on|off>";
608
+ readonly config_env_update_failed: "Runtime setting changed, but updating {value} failed: {error}";
595
609
  readonly requirements_title: "Config requirements";
596
610
  readonly requirements_empty: "No config requirements are configured.";
597
611
  readonly provider_title: "Model provider capabilities";
@@ -861,6 +875,8 @@ declare const MESSAGES: {
861
875
  readonly auth_repair_reverted: "已恢复之前的 auth。该候选仍需要修复。";
862
876
  readonly auth_candidate_deleted: "已删除 auth 候选:{value}";
863
877
  readonly auth_candidate_deleted_short: "auth 已删除";
878
+ readonly auth_candidate_auto_deleted: "已自动剔除无法恢复的 auth 候选:{value}";
879
+ readonly auth_pool_summary: "auth 池:历史 {total},存活 {alive},因失效剔除 {deleted}。";
864
880
  readonly auth_delete_current_restarted: "被删除的候选是当前 auth,已在删除后重启 Codex app-server。";
865
881
  readonly auth_no_candidates: "没有找到 auth 候选文件。请在 auth 目录中放置类似 auth.json_personal 的文件。";
866
882
  readonly auth_choice_expired: "这个 auth 列表已经不再有效";
@@ -869,6 +885,8 @@ declare const MESSAGES: {
869
885
  readonly auth_switching: "正在切换 Codex auth:{from} -> {to}...";
870
886
  readonly auth_switch_done: "Codex auth 已切换:{from} -> {to}。";
871
887
  readonly auth_switch_validation_failed: "选中的 auth 验证失败:{error}。已将 {value} 标记为需要登录修复。";
888
+ readonly auth_switch_validation_auto_deleted: "选中的 auth 验证失败:{error}。已自动剔除 {value}。";
889
+ readonly auth_switch_validation_auto_deleted_quiet: "选中的 auth 验证失败,已自动剔除。";
872
890
  readonly auth_switch_validation_reverted: "已在切换失败后恢复到之前的 auth。";
873
891
  readonly auth_recovered_newer_candidate: "重载前已从其他 Codex home 恢复 {value} 的同账号较新凭据。";
874
892
  readonly auth_refresh_all_confirm_short: "请先确认刷新全部风险。";
@@ -885,8 +903,10 @@ declare const MESSAGES: {
885
903
  readonly auth_refresh_all_skipped: "已跳过非 ChatGPT/无效候选:{value}";
886
904
  readonly auth_refresh_all_failed: "失败:{value}";
887
905
  readonly auth_auto_switching: "检测到 Codex auth 问题({error}),正在切换:{from} -> {to}...";
906
+ readonly auth_auto_switching_quiet: "检测到 Codex auth 问题({error}),正在选择另一个维护中的候选...";
888
907
  readonly auth_auto_recovered_current: "检测到 Codex auth 问题({error}),已为 {value} 恢复同账号较新凭据并重启 Codex app-server。";
889
908
  readonly auth_auto_done: "已自动切换 Codex auth:{from} -> {to}。";
909
+ readonly auth_auto_done_quiet: "Codex auth 问题已用另一个维护中的候选处理。";
890
910
  readonly auth_auto_retrying: "正在用新的 auth 重试同一条请求...";
891
911
  readonly auth_auto_retry_thread_missing: "Auth 已切换,但原线程已经不可用({threadId})。已停止重试,避免创建重复 session。";
892
912
  readonly auth_auto_no_candidate: "检测到 Codex auth 问题({error}),但没有可用的未失败候选 auth。";
@@ -899,6 +919,7 @@ declare const MESSAGES: {
899
919
  readonly auth_add_reverted: "已恢复之前的 auth。";
900
920
  readonly auth_add_missing_file: "登录已完成,但没有创建新的 auth 文件:{value}";
901
921
  readonly auth_sync_disabled: "跨节点 auth 同步未启用。";
922
+ readonly auth_sync_validation_busy: "正在验证跨节点 auth,并临时重启 Codex app-server。请稍等片刻后再发送这条消息。";
902
923
  readonly auth_sync_test_sent: "auth sync 测试完成:已发送 {sent},收到回应 {replied}。";
903
924
  readonly auth_sync_test_missing: "未回应:{value}";
904
925
  readonly auth_sync_push_blocked_active: "当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。";
@@ -936,6 +957,8 @@ declare const MESSAGES: {
936
957
  readonly button_auth_filter_all: "全部";
937
958
  readonly button_auth_filter_enabled: "已启用";
938
959
  readonly button_auth_filter_attention: "需关注";
960
+ readonly button_config_auth_auto_delete_on: "开启自动剔除";
961
+ readonly button_config_auth_auto_delete_off: "关闭自动剔除";
939
962
  readonly another_turn_running: "已经有一个回复在进行中。请先等待,或使用 /interrupt、/takeover、/queue。";
940
963
  readonly working: "处理中...";
941
964
  readonly usage_open: "用法:/open <编号>";
@@ -1266,6 +1289,11 @@ declare const MESSAGES: {
1266
1289
  readonly features_empty: "没有找到功能开关。";
1267
1290
  readonly config_title: "配置摘要";
1268
1291
  readonly config_layers: "配置层数:{count}";
1292
+ readonly config_foxclaw_title: "FoxClaw 运行时配置";
1293
+ readonly config_auth_auto_delete_needs_repair: "自动剔除无法恢复的 auth 候选:{value}";
1294
+ readonly config_auth_auto_delete_updated: "自动剔除无法恢复的 auth 候选已设置为:{value}";
1295
+ readonly config_auth_auto_delete_usage: "用法:/config auth_auto_delete <on|off>";
1296
+ readonly config_env_update_failed: "运行时设置已改变,但更新 {value} 失败:{error}";
1269
1297
  readonly requirements_title: "配置要求";
1270
1298
  readonly requirements_empty: "当前没有配置要求。";
1271
1299
  readonly provider_title: "模型供应商能力";
package/dist/i18n.js CHANGED
@@ -185,6 +185,8 @@ const MESSAGES = {
185
185
  auth_repair_reverted: 'Restored previous auth. The candidate still needs repair.',
186
186
  auth_candidate_deleted: 'Deleted auth candidate: {value}',
187
187
  auth_candidate_deleted_short: 'Auth deleted',
188
+ auth_candidate_auto_deleted: 'Auto-deleted unrecoverable auth candidate: {value}',
189
+ auth_pool_summary: 'Auth pool: total seen {total}, alive {alive}, invalid-deleted {deleted}.',
188
190
  auth_delete_current_restarted: 'The deleted candidate was current, so Codex app-server was restarted without it.',
189
191
  auth_no_candidates: 'No auth candidates found. Expected files like auth.json_personal in the auth dir.',
190
192
  auth_choice_expired: 'This auth list is no longer active',
@@ -193,6 +195,8 @@ const MESSAGES = {
193
195
  auth_switching: 'Switching Codex auth: {from} -> {to}...',
194
196
  auth_switch_done: 'Codex auth switched: {from} -> {to}.',
195
197
  auth_switch_validation_failed: 'Selected auth failed validation: {error}. Marked {value} for login repair.',
198
+ auth_switch_validation_auto_deleted: 'Selected auth failed validation: {error}. Auto-deleted {value}.',
199
+ auth_switch_validation_auto_deleted_quiet: 'Selected auth failed validation and was auto-deleted.',
196
200
  auth_switch_validation_reverted: 'Restored the previous auth after the failed switch.',
197
201
  auth_recovered_newer_candidate: 'Recovered a newer same-account credential for {value} from another Codex home before reload.',
198
202
  auth_refresh_all_confirm_short: 'Review refresh all risk first.',
@@ -209,8 +213,10 @@ const MESSAGES = {
209
213
  auth_refresh_all_skipped: 'Skipped non-ChatGPT/invalid candidates: {value}',
210
214
  auth_refresh_all_failed: 'Failed: {value}',
211
215
  auth_auto_switching: 'Codex auth problem detected ({error}). Switching: {from} -> {to}...',
216
+ auth_auto_switching_quiet: 'Codex auth problem detected ({error}). Selecting another maintained candidate...',
212
217
  auth_auto_recovered_current: 'Codex auth problem detected ({error}). Recovered a newer same-account credential for {value} and restarted Codex app-server.',
213
218
  auth_auto_done: 'Auto-switched Codex auth: {from} -> {to}.',
219
+ auth_auto_done_quiet: 'Codex auth problem handled with another maintained candidate.',
214
220
  auth_auto_retrying: 'Retrying the same request with the new auth...',
215
221
  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.',
216
222
  auth_auto_no_candidate: 'Codex auth problem detected ({error}), but no unused auth candidate is available.',
@@ -223,6 +229,7 @@ const MESSAGES = {
223
229
  auth_add_reverted: 'Restored previous auth.',
224
230
  auth_add_missing_file: 'Login completed, but the new auth file was not created: {value}',
225
231
  auth_sync_disabled: 'Cross-node auth sync is disabled.',
232
+ auth_sync_validation_busy: 'Auth sync is validating refreshed credentials and temporarily restarting Codex app-server. Wait a moment, then send this message again.',
226
233
  auth_sync_test_sent: 'Auth sync test complete: sent {sent}, replies {replied}.',
227
234
  auth_sync_test_missing: 'Missing replies: {value}',
228
235
  auth_sync_push_blocked_active: 'Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.',
@@ -260,6 +267,8 @@ const MESSAGES = {
260
267
  button_auth_filter_all: 'All',
261
268
  button_auth_filter_enabled: 'Enabled',
262
269
  button_auth_filter_attention: 'Attention',
270
+ button_config_auth_auto_delete_on: 'Auto-delete on',
271
+ button_config_auth_auto_delete_off: 'Auto-delete off',
263
272
  another_turn_running: 'Another turn is already running. Use /interrupt, /takeover, /queue, or wait.',
264
273
  working: 'Working...',
265
274
  usage_open: 'Usage: /open <n>',
@@ -590,6 +599,11 @@ const MESSAGES = {
590
599
  features_empty: 'No features found.',
591
600
  config_title: 'Config summary',
592
601
  config_layers: 'Config layers: {count}',
602
+ config_foxclaw_title: 'FoxClaw runtime config',
603
+ config_auth_auto_delete_needs_repair: 'Auto-delete unrecoverable auth candidates: {value}',
604
+ config_auth_auto_delete_updated: 'Auto-delete unrecoverable auth candidates set to: {value}',
605
+ config_auth_auto_delete_usage: 'Usage: /config auth_auto_delete <on|off>',
606
+ config_env_update_failed: 'Runtime setting changed, but updating {value} failed: {error}',
593
607
  requirements_title: 'Config requirements',
594
608
  requirements_empty: 'No config requirements are configured.',
595
609
  provider_title: 'Model provider capabilities',
@@ -859,6 +873,8 @@ const MESSAGES = {
859
873
  auth_repair_reverted: '已恢复之前的 auth。该候选仍需要修复。',
860
874
  auth_candidate_deleted: '已删除 auth 候选:{value}',
861
875
  auth_candidate_deleted_short: 'auth 已删除',
876
+ auth_candidate_auto_deleted: '已自动剔除无法恢复的 auth 候选:{value}',
877
+ auth_pool_summary: 'auth 池:历史 {total},存活 {alive},因失效剔除 {deleted}。',
862
878
  auth_delete_current_restarted: '被删除的候选是当前 auth,已在删除后重启 Codex app-server。',
863
879
  auth_no_candidates: '没有找到 auth 候选文件。请在 auth 目录中放置类似 auth.json_personal 的文件。',
864
880
  auth_choice_expired: '这个 auth 列表已经不再有效',
@@ -867,6 +883,8 @@ const MESSAGES = {
867
883
  auth_switching: '正在切换 Codex auth:{from} -> {to}...',
868
884
  auth_switch_done: 'Codex auth 已切换:{from} -> {to}。',
869
885
  auth_switch_validation_failed: '选中的 auth 验证失败:{error}。已将 {value} 标记为需要登录修复。',
886
+ auth_switch_validation_auto_deleted: '选中的 auth 验证失败:{error}。已自动剔除 {value}。',
887
+ auth_switch_validation_auto_deleted_quiet: '选中的 auth 验证失败,已自动剔除。',
870
888
  auth_switch_validation_reverted: '已在切换失败后恢复到之前的 auth。',
871
889
  auth_recovered_newer_candidate: '重载前已从其他 Codex home 恢复 {value} 的同账号较新凭据。',
872
890
  auth_refresh_all_confirm_short: '请先确认刷新全部风险。',
@@ -883,8 +901,10 @@ const MESSAGES = {
883
901
  auth_refresh_all_skipped: '已跳过非 ChatGPT/无效候选:{value}',
884
902
  auth_refresh_all_failed: '失败:{value}',
885
903
  auth_auto_switching: '检测到 Codex auth 问题({error}),正在切换:{from} -> {to}...',
904
+ auth_auto_switching_quiet: '检测到 Codex auth 问题({error}),正在选择另一个维护中的候选...',
886
905
  auth_auto_recovered_current: '检测到 Codex auth 问题({error}),已为 {value} 恢复同账号较新凭据并重启 Codex app-server。',
887
906
  auth_auto_done: '已自动切换 Codex auth:{from} -> {to}。',
907
+ auth_auto_done_quiet: 'Codex auth 问题已用另一个维护中的候选处理。',
888
908
  auth_auto_retrying: '正在用新的 auth 重试同一条请求...',
889
909
  auth_auto_retry_thread_missing: 'Auth 已切换,但原线程已经不可用({threadId})。已停止重试,避免创建重复 session。',
890
910
  auth_auto_no_candidate: '检测到 Codex auth 问题({error}),但没有可用的未失败候选 auth。',
@@ -897,6 +917,7 @@ const MESSAGES = {
897
917
  auth_add_reverted: '已恢复之前的 auth。',
898
918
  auth_add_missing_file: '登录已完成,但没有创建新的 auth 文件:{value}',
899
919
  auth_sync_disabled: '跨节点 auth 同步未启用。',
920
+ auth_sync_validation_busy: '正在验证跨节点 auth,并临时重启 Codex app-server。请稍等片刻后再发送这条消息。',
900
921
  auth_sync_test_sent: 'auth sync 测试完成:已发送 {sent},收到回应 {replied}。',
901
922
  auth_sync_test_missing: '未回应:{value}',
902
923
  auth_sync_push_blocked_active: '当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。',
@@ -934,6 +955,8 @@ const MESSAGES = {
934
955
  button_auth_filter_all: '全部',
935
956
  button_auth_filter_enabled: '已启用',
936
957
  button_auth_filter_attention: '需关注',
958
+ button_config_auth_auto_delete_on: '开启自动剔除',
959
+ button_config_auth_auto_delete_off: '关闭自动剔除',
937
960
  another_turn_running: '已经有一个回复在进行中。请先等待,或使用 /interrupt、/takeover、/queue。',
938
961
  working: '处理中...',
939
962
  usage_open: '用法:/open <编号>',
@@ -1264,6 +1287,11 @@ const MESSAGES = {
1264
1287
  features_empty: '没有找到功能开关。',
1265
1288
  config_title: '配置摘要',
1266
1289
  config_layers: '配置层数:{count}',
1290
+ config_foxclaw_title: 'FoxClaw 运行时配置',
1291
+ config_auth_auto_delete_needs_repair: '自动剔除无法恢复的 auth 候选:{value}',
1292
+ config_auth_auto_delete_updated: '自动剔除无法恢复的 auth 候选已设置为:{value}',
1293
+ config_auth_auto_delete_usage: '用法:/config auth_auto_delete <on|off>',
1294
+ config_env_update_failed: '运行时设置已改变,但更新 {value} 失败:{error}',
1267
1295
  requirements_title: '配置要求',
1268
1296
  requirements_empty: '当前没有配置要求。',
1269
1297
  provider_title: '模型供应商能力',
package/dist/main.js CHANGED
@@ -253,7 +253,9 @@ async function runServeCli() {
253
253
  label: runtime.bot.username ? `@${runtime.bot.username}` : runtime.id,
254
254
  authDir: runtime.authDir,
255
255
  validate: async (context) => validateRefreshedAuthCandidate(runtime, context.candidateName),
256
- notify: createAuthMirrorNotifier(store, runtime.id, runtime.bot, authNotificationAggregator),
256
+ notify: createAuthMirrorNotifier(store, runtime.id, runtime.bot, authNotificationAggregator, {
257
+ quietAuthPoolMode: () => config.authAutoDeleteNeedsRepair,
258
+ }),
257
259
  })), logger, path.join(APP_HOME, 'runtime', 'auth-mirror.json'), {
258
260
  onSynced: async (event) => {
259
261
  await authSync?.publishCandidate(event.record.candidateName);
@@ -324,7 +326,10 @@ async function runServeCli() {
324
326
  canSelfUpdate: () => authSyncLocalIdle()
325
327
  && (authSync ? authSync.isIdle() : localAuthRefreshLease.isIdle()),
326
328
  authCandidateUpdated: (runtimeId, candidateName) => mirror.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined),
327
- authCandidateDeleted: (_runtimeId, candidateName) => mirror.deleteCandidate(candidateName).then(() => undefined),
329
+ authCandidateDeleted: async (_runtimeId, candidateName, reason = null) => {
330
+ await mirror.deleteCandidate(candidateName);
331
+ await authSync?.publishCandidateDeletion(candidateName, reason);
332
+ },
328
333
  recoverAuthCandidate: async (runtimeId, candidateName, options = {}) => {
329
334
  const local = await mirror.recoverRuntimeCandidate(runtimeId, candidateName);
330
335
  if (local)
@@ -424,8 +429,22 @@ async function runServeCli() {
424
429
  return runtime.core.validateExternalCodexAuthCandidate(candidateName, raw, expectedAccountId);
425
430
  },
426
431
  importCandidate: (candidateName, raw, source) => mirror.importExternalCandidate(candidateName, raw, source),
432
+ deleteLocalCandidate: async (candidateName, source) => {
433
+ if (!authSyncLocalIdle()) {
434
+ return { ok: false, deleted: false, reason: 'runtime is not idle' };
435
+ }
436
+ await mirror.deleteCandidate(candidateName);
437
+ store.deleteCodexAuthCandidate(candidateName);
438
+ await Promise.all(runtimes.map(runtime => runtime.core.handleExternalCodexAuthCandidateDeleted(candidateName, source.reason ?? null)));
439
+ if (activeWeixinCore) {
440
+ await activeWeixinCore.handleExternalCodexAuthCandidateDeleted(candidateName, source.reason ?? null);
441
+ }
442
+ return { ok: true, deleted: true, reason: source.reason ?? null };
443
+ },
427
444
  isIdle: authSyncLocalIdle,
428
- notify: createAuthSyncNotifier(store, authSyncTransportBot.bot, authNotificationAggregator),
445
+ notify: createAuthSyncNotifier(store, authSyncTransportBot.bot, authNotificationAggregator, {
446
+ quietAuthPoolMode: () => config.authAutoDeleteNeedsRepair,
447
+ }),
429
448
  });
430
449
  await authSync.initialize();
431
450
  activeAuthSync = authSync;
@@ -494,7 +513,10 @@ async function runServeCli() {
494
513
  canSelfUpdate: () => singleAuthSyncLocalIdle()
495
514
  && (singleAuthSync ? singleAuthSync.isIdle() : singleLocalAuthRefreshLease.isIdle()),
496
515
  authCandidateUpdated: (runtimeId, candidateName) => singleMirror?.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined) ?? Promise.resolve(),
497
- authCandidateDeleted: (_runtimeId, candidateName) => singleMirror?.deleteCandidate(candidateName).then(() => undefined) ?? Promise.resolve(),
516
+ authCandidateDeleted: async (_runtimeId, candidateName, reason = null) => {
517
+ await singleMirror?.deleteCandidate(candidateName);
518
+ await singleAuthSync?.publishCandidateDeletion(candidateName, reason);
519
+ },
498
520
  recoverAuthCandidate: async (runtimeId, candidateName, options = {}) => {
499
521
  const local = await singleMirror?.recoverRuntimeCandidate(runtimeId, candidateName) ?? null;
500
522
  if (local)
@@ -570,8 +592,19 @@ async function runServeCli() {
570
592
  return core.validateExternalCodexAuthCandidate(candidateName, raw, expectedAccountId);
571
593
  },
572
594
  importCandidate: (candidateName, raw, source) => singleMirror.importExternalCandidate(candidateName, raw, source),
595
+ deleteLocalCandidate: async (candidateName, source) => {
596
+ if (!singleAuthSyncLocalIdle()) {
597
+ return { ok: false, deleted: false, reason: 'runtime is not idle' };
598
+ }
599
+ await singleMirror.deleteCandidate(candidateName);
600
+ store.deleteCodexAuthCandidate(candidateName);
601
+ await core.handleExternalCodexAuthCandidateDeleted(candidateName, source.reason ?? null);
602
+ return { ok: true, deleted: true, reason: source.reason ?? null };
603
+ },
573
604
  isIdle: singleAuthSyncLocalIdle,
574
- notify: createAuthSyncNotifier(store, bot, authNotificationAggregator),
605
+ notify: createAuthSyncNotifier(store, bot, authNotificationAggregator, {
606
+ quietAuthPoolMode: () => config.authAutoDeleteNeedsRepair,
607
+ }),
575
608
  });
576
609
  await singleAuthSync.initialize();
577
610
  activeAuthSync = singleAuthSync;
@@ -641,8 +674,10 @@ async function runServeCli() {
641
674
  throw error;
642
675
  }
643
676
  }
644
- function createAuthMirrorNotifier(store, botId, bot, aggregator) {
677
+ function createAuthMirrorNotifier(store, botId, bot, aggregator, options = {}) {
645
678
  return async (event) => {
679
+ if (options.quietAuthPoolMode?.())
680
+ return;
646
681
  const privateScope = store.getTelegramPrivateScope(botId);
647
682
  if (!privateScope)
648
683
  return;
@@ -654,7 +689,27 @@ function createAuthMirrorNotifier(store, botId, bot, aggregator) {
654
689
  }, event);
655
690
  };
656
691
  }
657
- function createAuthSyncNotifier(store, bot, aggregator) {
692
+ function createAuthSyncNotifier(store, bot, aggregator, options = {}) {
693
+ const poolSummaryQueues = new Map();
694
+ const enqueuePoolSummary = (destination) => {
695
+ const existing = poolSummaryQueues.get(destination.key);
696
+ if (existing) {
697
+ clearTimeout(existing.timer);
698
+ }
699
+ const timer = setTimeout(() => {
700
+ const queued = poolSummaryQueues.get(destination.key);
701
+ if (!queued)
702
+ return;
703
+ poolSummaryQueues.delete(destination.key);
704
+ void queued.sendMessage(formatAuthPoolSummary(queued.locale, store.getCodexAuthPoolStats())).catch(() => undefined);
705
+ }, 2_000);
706
+ timer.unref();
707
+ poolSummaryQueues.set(destination.key, {
708
+ locale: destination.locale,
709
+ sendMessage: destination.sendMessage,
710
+ timer,
711
+ });
712
+ };
658
713
  return async (event) => {
659
714
  if (!bot.identity)
660
715
  return;
@@ -667,6 +722,12 @@ function createAuthSyncNotifier(store, bot, aggregator) {
667
722
  locale,
668
723
  sendMessage: (text) => bot.sendMessage(privateScope.chatId, text),
669
724
  };
725
+ if (options.quietAuthPoolMode?.() && isQuietAuthPoolNotification(event)) {
726
+ if (shouldSendQuietAuthPoolSummary(event)) {
727
+ enqueuePoolSummary(destination);
728
+ }
729
+ return;
730
+ }
670
731
  if (aggregator.enqueueAuthSync(destination, event)) {
671
732
  return;
672
733
  }
@@ -676,6 +737,45 @@ function createAuthSyncNotifier(store, bot, aggregator) {
676
737
  function authNotificationDestinationKey(botId, chatId) {
677
738
  return `${botId}:${chatId}`;
678
739
  }
740
+ function formatAuthPoolSummary(locale, stats) {
741
+ return locale === 'zh'
742
+ ? `auth 池:历史 ${stats.totalSeen},存活 ${stats.alive},因失效剔除 ${stats.deletedInvalid}。`
743
+ : `Auth pool: total seen ${stats.totalSeen}, alive ${stats.alive}, invalid-deleted ${stats.deletedInvalid}.`;
744
+ }
745
+ function isQuietAuthPoolNotification(event) {
746
+ switch (event.kind) {
747
+ case 'candidate_publish_started':
748
+ case 'candidate_publish_completed':
749
+ case 'push_all_started':
750
+ case 'push_all_completed':
751
+ case 'remote_bundle_received':
752
+ case 'remote_import_imported':
753
+ case 'remote_import_skipped':
754
+ case 'candidate_delete_sent':
755
+ case 'remote_delete_received':
756
+ case 'remote_delete_deleted':
757
+ case 'remote_delete_skipped':
758
+ case 'recovery_started':
759
+ case 'recovery_peer_empty':
760
+ case 'recovery_peer_bundle_received':
761
+ case 'recovery_failed':
762
+ case 'pull_request_received':
763
+ case 'pull_response_sent':
764
+ return true;
765
+ default:
766
+ return false;
767
+ }
768
+ }
769
+ function shouldSendQuietAuthPoolSummary(event) {
770
+ switch (event.kind) {
771
+ case 'candidate_delete_sent':
772
+ case 'remote_delete_deleted':
773
+ case 'remote_delete_skipped':
774
+ return true;
775
+ default:
776
+ return false;
777
+ }
778
+ }
679
779
  function formatAuthSyncNotification(locale, event) {
680
780
  const peers = 'peers' in event ? formatPeerList(event.peers, locale) : '';
681
781
  if (locale === 'zh') {
@@ -704,6 +804,23 @@ function formatAuthSyncNotification(locale, event) {
704
804
  return `${event.mode === 'pull' ? '跨节点拉取未改动本机文件' : '收到跨节点 auth 但未写盘'}:${event.candidateName}\n来源:${formatSource(event.sourceLabel, event.sourceNodeId)}\n原因:${event.reason}`;
705
805
  case 'remote_import_failed':
706
806
  return `${event.mode === 'pull' ? '跨节点拉取导入失败' : '跨节点 auth 导入失败'}:${event.candidateName}\n来源:${formatSource(event.sourceLabel, event.sourceNodeId)}\n原因:${event.reason}\n需要注意:如果其他候选也无法恢复,请人工介入重新登录或刷新这个 auth。`;
807
+ case 'candidate_delete_sent':
808
+ return `跨节点 auth 删除已发出:${event.candidateName}\nPeer:${peers}${event.reason ? `\n原因:${event.reason}` : ''}`;
809
+ case 'candidate_delete_failed':
810
+ return `跨节点 auth 删除发送失败:${event.candidateName}\nPeer:${peers}\n原因:${event.reason}`;
811
+ case 'remote_delete_received':
812
+ return [
813
+ `收到跨节点 auth 删除:${event.candidateName}`,
814
+ `来源:${formatSource(event.sourceLabel, event.sourceNodeId)},peer ${event.peer}`,
815
+ `处理:${event.queued ? `本机忙,已排队等待空闲后删除;当前待删除 ${event.queueLength}` : '本机空闲,正在删除同名候选'}`,
816
+ event.reason ? `原因:${event.reason}` : null,
817
+ ].filter(Boolean).join('\n');
818
+ case 'remote_delete_deleted':
819
+ return `已执行跨节点 auth 删除:${event.candidateName}\n来源:${formatSource(event.sourceLabel, event.sourceNodeId)}${event.reason ? `\n原因:${event.reason}` : ''}`;
820
+ case 'remote_delete_skipped':
821
+ return `跨节点 auth 删除未改动本机文件:${event.candidateName}\n来源:${formatSource(event.sourceLabel, event.sourceNodeId)}\n原因:${event.reason}`;
822
+ case 'remote_delete_failed':
823
+ return `跨节点 auth 删除失败:${event.candidateName}\n来源:${formatSource(event.sourceLabel, event.sourceNodeId)}\n原因:${event.reason}`;
707
824
  case 'recovery_started':
708
825
  return `auth 恢复开始:${event.candidateName}\nRequest:${event.requestId}\n处理:同节点没有可用较新副本,正在向跨节点 peer 查询:${peers},最长等待 ${event.timeoutMs}ms`;
709
826
  case 'recovery_peer_empty':
@@ -753,6 +870,23 @@ function formatAuthSyncNotification(locale, event) {
753
870
  return `${event.mode === 'pull' ? 'Cross-node pull did not change local files' : 'Received cross-node auth but did not write it'}: ${event.candidateName}\nSource: ${formatSource(event.sourceLabel, event.sourceNodeId)}\nReason: ${event.reason}`;
754
871
  case 'remote_import_failed':
755
872
  return `${event.mode === 'pull' ? 'Cross-node pull import failed' : 'Cross-node auth import failed'}: ${event.candidateName}\nSource: ${formatSource(event.sourceLabel, event.sourceNodeId)}\nReason: ${event.reason}\nAttention: if no other candidate can recover this account, run device login or refresh this auth manually.`;
873
+ case 'candidate_delete_sent':
874
+ return `Cross-node auth delete sent: ${event.candidateName}\nPeers: ${peers}${event.reason ? `\nReason: ${event.reason}` : ''}`;
875
+ case 'candidate_delete_failed':
876
+ return `Cross-node auth delete send failed: ${event.candidateName}\nPeers: ${peers}\nReason: ${event.reason}`;
877
+ case 'remote_delete_received':
878
+ return [
879
+ `Received cross-node auth delete: ${event.candidateName}`,
880
+ `Source: ${formatSource(event.sourceLabel, event.sourceNodeId)}, peer ${event.peer}`,
881
+ `Action: ${event.queued ? `queued until this node is idle; pending deletes ${event.queueLength}` : 'deleting the matching local candidate'}`,
882
+ event.reason ? `Reason: ${event.reason}` : null,
883
+ ].filter(Boolean).join('\n');
884
+ case 'remote_delete_deleted':
885
+ return `Applied cross-node auth delete: ${event.candidateName}\nSource: ${formatSource(event.sourceLabel, event.sourceNodeId)}${event.reason ? `\nReason: ${event.reason}` : ''}`;
886
+ case 'remote_delete_skipped':
887
+ return `Cross-node auth delete did not change local files: ${event.candidateName}\nSource: ${formatSource(event.sourceLabel, event.sourceNodeId)}\nReason: ${event.reason}`;
888
+ case 'remote_delete_failed':
889
+ return `Cross-node auth delete failed: ${event.candidateName}\nSource: ${formatSource(event.sourceLabel, event.sourceNodeId)}\nReason: ${event.reason}`;
756
890
  case 'recovery_started':
757
891
  return `Auth recovery started: ${event.candidateName}\nRequest: ${event.requestId}\nAction: no newer same-node copy was available; querying cross-node peers: ${peers}; timeout ${event.timeoutMs}ms`;
758
892
  case 'recovery_peer_empty':
@@ -38,6 +38,11 @@ export interface CodexAuthQuotaSnapshotRecord {
38
38
  secondaryRemainingPercent: number | null;
39
39
  updatedAt: number;
40
40
  }
41
+ export interface CodexAuthPoolStats {
42
+ totalSeen: number;
43
+ alive: number;
44
+ deletedInvalid: number;
45
+ }
41
46
  export type CodexAuthCandidateState = 'active' | 'needs_repair';
42
47
  export declare class BridgeStore {
43
48
  private db;
@@ -125,6 +130,10 @@ export declare class BridgeStore {
125
130
  setCodexAuthCandidateDisabled(name: string, disabled: boolean, runtimeId?: string): void;
126
131
  setCodexAuthCandidateState(name: string, state: CodexAuthCandidateState, runtimeId?: string): void;
127
132
  deleteCodexAuthCandidate(name: string): void;
133
+ recordCodexAuthPoolInventory(names: string[]): void;
134
+ recordCodexAuthCandidateInvalidDelete(name: string, reason?: string | null): void;
135
+ recordCodexAuthCandidateRemoved(name: string, reason?: string | null): void;
136
+ getCodexAuthPoolStats(): CodexAuthPoolStats;
128
137
  setCodexAuthQuotaSnapshot(runtimeId: string, candidateName: string, accountId: string, quotaIdentityId: string, snapshot: Pick<CodexAuthQuotaSnapshotRecord, 'capturedAtMs' | 'planType' | 'primaryWindowDurationMins' | 'primaryRemainingPercent' | 'secondaryWindowDurationMins' | 'secondaryRemainingPercent'>): void;
129
138
  listCodexAuthQuotaSnapshots(quotaIdentityIds: string[]): CodexAuthQuotaSnapshotRecord[];
130
139
  private ensureColumn;
@@ -197,6 +197,14 @@ export class BridgeStore {
197
197
  );
198
198
  CREATE INDEX IF NOT EXISTS codex_auth_quota_snapshots_account_idx
199
199
  ON codex_auth_quota_snapshots(account_id);
200
+ CREATE TABLE IF NOT EXISTS codex_auth_pool_history (
201
+ name TEXT PRIMARY KEY,
202
+ first_seen_at INTEGER NOT NULL,
203
+ last_seen_at INTEGER NOT NULL,
204
+ deleted_at INTEGER,
205
+ delete_reason TEXT,
206
+ invalid_delete_count INTEGER NOT NULL DEFAULT 0
207
+ );
200
208
  `);
201
209
  this.ensureColumn('thread_cache', 'name', 'TEXT');
202
210
  this.ensureColumn('thread_cache', 'model_provider', 'TEXT');
@@ -217,6 +225,7 @@ export class BridgeStore {
217
225
  this.ensureColumn('codex_auth_quota_snapshots', 'primary_window_duration_mins', 'REAL');
218
226
  this.ensureColumn('codex_auth_quota_snapshots', 'secondary_window_duration_mins', 'REAL');
219
227
  this.ensureColumn('codex_auth_quota_snapshots', 'quota_identity_id', "TEXT NOT NULL DEFAULT ''");
228
+ this.ensureColumn('codex_auth_pool_history', 'invalid_delete_count', 'INTEGER NOT NULL DEFAULT 0');
220
229
  this.db.prepare(`
221
230
  UPDATE codex_auth_quota_snapshots
222
231
  SET quota_identity_id = account_id
@@ -896,6 +905,84 @@ export class BridgeStore {
896
905
  this.db.prepare('DELETE FROM codex_auth_candidate_runtime WHERE name = ?').run(name);
897
906
  this.db.prepare('DELETE FROM codex_auth_quota_snapshots WHERE candidate_name = ?').run(name);
898
907
  }
908
+ recordCodexAuthPoolInventory(names) {
909
+ if (names.length === 0) {
910
+ return;
911
+ }
912
+ const now = Date.now();
913
+ const upsert = this.db.prepare(`
914
+ INSERT INTO codex_auth_pool_history (name, first_seen_at, last_seen_at, deleted_at, delete_reason, invalid_delete_count)
915
+ VALUES (?, ?, ?, NULL, NULL, 0)
916
+ ON CONFLICT(name) DO UPDATE SET
917
+ last_seen_at = excluded.last_seen_at,
918
+ deleted_at = NULL,
919
+ delete_reason = NULL
920
+ `);
921
+ const seen = new Set(names);
922
+ this.db.exec('BEGIN');
923
+ try {
924
+ for (const name of seen) {
925
+ upsert.run(name, now, now);
926
+ }
927
+ this.db.exec('COMMIT');
928
+ }
929
+ catch (error) {
930
+ this.db.exec('ROLLBACK');
931
+ throw error;
932
+ }
933
+ }
934
+ recordCodexAuthCandidateInvalidDelete(name, reason = null) {
935
+ const now = Date.now();
936
+ this.db.prepare(`
937
+ INSERT INTO codex_auth_pool_history (
938
+ name,
939
+ first_seen_at,
940
+ last_seen_at,
941
+ deleted_at,
942
+ delete_reason,
943
+ invalid_delete_count
944
+ )
945
+ VALUES (?, ?, ?, ?, ?, 1)
946
+ ON CONFLICT(name) DO UPDATE SET
947
+ last_seen_at = excluded.last_seen_at,
948
+ deleted_at = excluded.deleted_at,
949
+ delete_reason = excluded.delete_reason,
950
+ invalid_delete_count = codex_auth_pool_history.invalid_delete_count
951
+ + CASE WHEN codex_auth_pool_history.deleted_at IS NULL THEN 1 ELSE 0 END
952
+ `).run(name, now, now, now, reason);
953
+ }
954
+ recordCodexAuthCandidateRemoved(name, reason = null) {
955
+ const now = Date.now();
956
+ this.db.prepare(`
957
+ INSERT INTO codex_auth_pool_history (
958
+ name,
959
+ first_seen_at,
960
+ last_seen_at,
961
+ deleted_at,
962
+ delete_reason,
963
+ invalid_delete_count
964
+ )
965
+ VALUES (?, ?, ?, ?, ?, 0)
966
+ ON CONFLICT(name) DO UPDATE SET
967
+ last_seen_at = excluded.last_seen_at,
968
+ deleted_at = excluded.deleted_at,
969
+ delete_reason = excluded.delete_reason
970
+ `).run(name, now, now, now, reason);
971
+ }
972
+ getCodexAuthPoolStats() {
973
+ const row = this.db.prepare(`
974
+ SELECT
975
+ COUNT(*) AS total_seen,
976
+ COALESCE(SUM(CASE WHEN deleted_at IS NULL THEN 1 ELSE 0 END), 0) AS alive,
977
+ COALESCE(SUM(invalid_delete_count), 0) AS deleted_invalid
978
+ FROM codex_auth_pool_history
979
+ `).get();
980
+ return {
981
+ totalSeen: Number(row?.total_seen ?? 0),
982
+ alive: Number(row?.alive ?? 0),
983
+ deletedInvalid: Number(row?.deleted_invalid ?? 0),
984
+ };
985
+ }
899
986
  setCodexAuthQuotaSnapshot(runtimeId, candidateName, accountId, quotaIdentityId, snapshot) {
900
987
  this.db.prepare(`
901
988
  INSERT INTO codex_auth_quota_snapshots (
@@ -92,6 +92,7 @@ AUTH_SYNC_KEY=<shared key with at least 32 bytes>
92
92
  AUTH_SYNC_CLUSTER_ID=my-codex-auth-pool
93
93
  AUTH_SYNC_NODE_ID=workstation-a
94
94
  AUTH_SYNC_PEERS=@foxclaw_node_b_bot
95
+ AUTH_AUTO_DELETE_NEEDS_REPAIR=false
95
96
  ```
96
97
 
97
98
  Node B:
@@ -103,6 +104,7 @@ AUTH_SYNC_KEY=<shared key with at least 32 bytes>
103
104
  AUTH_SYNC_CLUSTER_ID=my-codex-auth-pool
104
105
  AUTH_SYNC_NODE_ID=workstation-b
105
106
  AUTH_SYNC_PEERS=@foxclaw_node_a_bot
107
+ AUTH_AUTO_DELETE_NEEDS_REPAIR=false
106
108
  ```
107
109
 
108
110
  For more peers, separate bot usernames with commas:
@@ -166,7 +168,9 @@ Confirm that pending imports were processed, or that the candidate exists or has
166
168
 
167
169
  Note: `/auth sync push all` saying “sent” only means this node successfully handed encrypted packages to Telegram. It does not prove the peer wrote files. The peer imports only when it is globally idle, usage validation succeeds, same-name candidates belong to the same account id and compatible ChatGPT user/email identity, and the remote `last_refresh` is newer than the local copy. If the local file is already equal or newer, it will not change and `Last import` may remain empty.
168
170
 
169
- When cross-node sync is enabled, the contact bot private chat receives node-level notifications: local auth updates and the peers being contacted, received remote bundles and whether they were queued or immediately validated, import success/skip/failure reasons, recovery peer queries and peer replies, and a manual-intervention notice when every peer lacks an importable copy. Refresh/send/import bursts are grouped into short summaries so one candidate update does not produce separate start, receive, mirror-write, and completion messages. Recovery and manual-intervention notices remain explicit. Notifications never include auth contents, tokens, or encrypted bundle payloads.
171
+ When cross-node sync is enabled, the contact bot private chat receives node-level notifications: local auth updates and the peers being contacted, received remote bundles and whether they were queued or immediately validated, import success/skip/failure reasons, recovery peer queries and peer replies, and a manual-intervention notice when every peer lacks an importable copy. Refresh/send/import bursts are grouped into short summaries so one candidate update does not produce separate start, receive, mirror-write, and completion messages. Recovery and manual-intervention notices remain explicit. Remote import validation temporarily restarts the local Codex app-server; during that restart window FoxClaw treats the runtime as non-idle and asks ordinary messages to be resent shortly instead of running them against a restarting bridge. Notifications never include auth contents, tokens, or encrypted bundle payloads.
172
+
173
+ For a resource-rich pool where individual candidate maintenance is not important, enable `AUTH_AUTO_DELETE_NEEDS_REPAIR=true` or toggle it with `/config auth_auto_delete on`. Candidates that cannot be recovered and would otherwise be marked for repair are deleted locally, a delete tombstone is sent to peers, and candidate-level sync/delete chatter is collapsed into an auth-pool summary.
170
174
 
171
175
  Starting in 0.5.2, `/auth sync status` separates sync-system `Last error` from per-auth `Candidate failures`. For example, a remote candidate that returns `token_invalidated` or has an expired access token is recorded under that candidate name only; current `auth.json` health is still determined by validating the current auth usage. `local candidate is already newer or equal` is a normal skip, not an error.
172
176