@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 CHANGED
@@ -48,6 +48,10 @@ CODEX_CLI_BIN=/absolute/path/to/codex
48
48
  # AUTH_SYNC_PEERS=@other_node_bot,@third_node_bot
49
49
  # AUTH_SYNC_CLUSTER_ID=my-codex-auth-pool
50
50
  # AUTH_SYNC_NODE_ID=
51
+ # Resource-rich auth pool mode: unrecoverable candidates that would need repair
52
+ # are deleted automatically and propagated across auth-sync peers. /config can
53
+ # toggle this at runtime and writes AUTH_AUTO_DELETE_NEEDS_REPAIR back here.
54
+ # AUTH_AUTO_DELETE_NEEDS_REPAIR=false
51
55
 
52
56
  # Optional: standard HTTP(S) proxy for Telegram and ChatGPT/Codex backend requests.
53
57
  # Put these in the same env file that `foxclaw start` installs into systemd/launchd.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  All notable FoxClaw changes are listed here. Each release note is bilingual so GitHub Releases and the npm package are useful to both Chinese and English readers.
4
4
 
5
+ ## 0.5.22 - 2026-06-09
6
+
7
+ ### 中文
8
+ - Codex 报 `usageLimitExceeded`、`You've hit your usage limit`、usage/rate/quota/billing/credits limit 这类额度耗尽时,不再把当前 auth 候选标记为需要修复,也不会触发 `AUTH_AUTO_DELETE_NEEDS_REPAIR` 的自动剔除和跨节点删除。
9
+ - 额度耗尽仍会临时避开当前候选,切到另一个维护中的候选重试;提示文案改为“Codex 额度限制”,不再误写成 “Codex auth 问题”。
10
+
11
+ ### English
12
+ - Codex quota exhaustion such as `usageLimitExceeded`, `You've hit your usage limit`, usage/rate/quota/billing/credits limit is no longer treated as an invalid auth candidate, so it does not mark `needs_repair` or trigger `AUTH_AUTO_DELETE_NEEDS_REPAIR` auto-delete / cross-node delete.
13
+ - Quota exhaustion still temporarily skips the current candidate and retries with another maintained candidate; notifications now call this a Codex usage limit instead of an auth problem.
14
+
15
+ ## 0.5.21 - 2026-06-09
16
+
17
+ ### 中文
18
+ - 新增资源富裕 auth 池模式:`AUTH_AUTO_DELETE_NEEDS_REPAIR=true` 或 `/config auth_auto_delete on` 会把原本要标记为 `?`/需要登录修复、且无法恢复的候选自动剔除。
19
+ - 自动剔除会通过跨节点 auth sync 发送删除 tombstone,peer 收到后删除同名候选,并优先处理删除,避免待导入队列把坏候选复活。
20
+ - `/status` 和 `/config` 现在显示 auth 池摘要:历史见过的候选数、当前存活数、因失效自动剔除数;`/config` 的开关会写回当前 FoxClaw `.env`。
21
+ - 开启自动剔除后,auth mirror / auth sync 的候选级同步、导入、删除和恢复通知会静默或汇总为池子摘要;同步发送/导入失败、删除失败和系统级 `sync_error` 仍会明确提示。
22
+
23
+ ### English
24
+ - Added a resource-rich auth pool mode: `AUTH_AUTO_DELETE_NEEDS_REPAIR=true` or `/config auth_auto_delete on` automatically deletes unrecoverable candidates that would otherwise be marked `?` / needs login repair.
25
+ - Auto-delete now publishes a cross-node auth-sync delete tombstone, so peers delete the same candidate and process deletes before pending imports to avoid resurrecting bad candidates.
26
+ - `/status` and `/config` now show an auth-pool summary: total candidates seen, currently alive, and invalid-deleted count. The `/config` toggle writes back to the active FoxClaw `.env`.
27
+ - When auto-delete is enabled, candidate-level auth mirror / auth sync publish, import, delete, and recovery chatter is silenced or collapsed into pool summaries; sync send/import failures, delete failures, and system-level `sync_error` still notify explicitly.
28
+
5
29
  ## 0.5.20 - 2026-06-08
6
30
 
7
31
  ### 中文
package/README.md CHANGED
@@ -216,7 +216,7 @@ TG_BOT_TOKEN=123456:token_a
216
216
 
217
217
  FoxClaw 仍然只运行一个系统服务。默认情况下,它会为每个 bot 启动独立 `codex app-server` 和独立 `CODEX_HOME`。因此 A 私聊运行 turn 时,B 私聊仍可独立切换自己的 `/auth`。候选凭据由 FoxClaw 在登录或刷新在线验证后镜像同步;切换或重载前还会从其他 Codex home 恢复同账号较新凭据。各 bot 的当前选择互不影响。每个 bot 首次私聊发送 `/help` 和 `/status`;`/auth` 会标明正在操作的 bot runtime,`/status` 会列出全部 bot 的连接、runtime 类型、当前 auth 和活动 turn 摘要。
218
218
 
219
- 多台机器共享同一合法账号池时,可以启用可选跨节点 auth 同步:`AUTH_SYNC_ENABLED=true`、`AUTH_SYNC_KEY` 和 `AUTH_SYNC_PEERS=@peer_contact_bot`。推荐每台机器只选一个联系人 bot;同一节点内的其他 bot 继续走本机 auth 镜像。多 bot 模式下,默认用 `TG_BOT_TOKENS` 的第一个 token 作为联系人 bot。FoxClaw 会通过 Telegram Bot-to-Bot 私聊传输加密 auth 包;本机验证刷新后主动 push,发现本机候选失效时主动 pull peer 已持有的有效副本,并在联系人 bot 私聊里汇总报告发送、接收、排队、导入、失败和人工介入提示。`/auth sync events [过滤]` 和 `/auth sync trace <requestId>` 可查看最近通讯流水。跨节点恢复不会自动刷新 token,`/auth refresh all confirm` 会先申请跨节点刷新锁。完整配置、`@BotFather` 操作和验证步骤见 [跨节点 auth 同步配置指南](./docs/zh/cross-node-auth-sync.md)。
219
+ 多台机器共享同一合法账号池时,可以启用可选跨节点 auth 同步:`AUTH_SYNC_ENABLED=true`、`AUTH_SYNC_KEY` 和 `AUTH_SYNC_PEERS=@peer_contact_bot`。推荐每台机器只选一个联系人 bot;同一节点内的其他 bot 继续走本机 auth 镜像。多 bot 模式下,默认用 `TG_BOT_TOKENS` 的第一个 token 作为联系人 bot。FoxClaw 会通过 Telegram Bot-to-Bot 私聊传输加密 auth 包;本机验证刷新后主动 push,发现本机候选失效时主动 pull peer 已持有的有效副本,并在联系人 bot 私聊里汇总报告发送、接收、排队、导入、失败和人工介入提示。资源富裕、只关心池子数量时,可用 `/config auth_auto_delete on` 或 `AUTH_AUTO_DELETE_NEEDS_REPAIR=true` 让无法恢复的候选自动跨节点剔除,并把通知压缩为池子摘要。`/auth sync events [过滤]` 和 `/auth sync trace <requestId>` 可查看最近通讯流水。跨节点恢复不会自动刷新 token,`/auth refresh all confirm` 会先申请跨节点刷新锁。完整配置、`@BotFather` 操作和验证步骤见 [跨节点 auth 同步配置指南](./docs/zh/cross-node-auth-sync.md)。
220
220
 
221
221
  如果你需要一路 Telegram 与终端互通 session,把同一个 token 同时填入 `TG_BOT_TOKENS` 和 `TG_BOT_TOKEN`。这个 bot 使用默认 `CODEX_HOME`(未设置时通常是 `~/.codex`)和默认 auth,因此能看到终端 Codex 的本地线程;它不再享有隔离 runtime 的“互不影响”保证,切换 auth 会影响终端和其他默认 runtime。
222
222
 
@@ -123,6 +123,46 @@ export type AuthSyncNotification = {
123
123
  peer: string;
124
124
  mode: AuthSyncRemoteImportMode;
125
125
  reason: string;
126
+ } | {
127
+ kind: 'candidate_delete_sent';
128
+ candidateName: string;
129
+ peers: string[];
130
+ reason: string | null;
131
+ } | {
132
+ kind: 'candidate_delete_failed';
133
+ candidateName: string;
134
+ peers: string[];
135
+ reason: string;
136
+ } | {
137
+ kind: 'remote_delete_received';
138
+ candidateName: string;
139
+ sourceNodeId: string;
140
+ sourceLabel: string;
141
+ peer: string;
142
+ queued: boolean;
143
+ queueLength: number;
144
+ reason: string | null;
145
+ } | {
146
+ kind: 'remote_delete_deleted';
147
+ candidateName: string;
148
+ sourceNodeId: string;
149
+ sourceLabel: string;
150
+ peer: string;
151
+ reason: string | null;
152
+ } | {
153
+ kind: 'remote_delete_skipped';
154
+ candidateName: string;
155
+ sourceNodeId: string;
156
+ sourceLabel: string;
157
+ peer: string;
158
+ reason: string;
159
+ } | {
160
+ kind: 'remote_delete_failed';
161
+ candidateName: string;
162
+ sourceNodeId: string;
163
+ sourceLabel: string;
164
+ peer: string;
165
+ reason: string;
126
166
  } | {
127
167
  kind: 'recovery_started';
128
168
  candidateName: string;
@@ -170,6 +210,15 @@ export interface AuthSyncImportCallbacks {
170
210
  nodeId: string;
171
211
  label?: string | null;
172
212
  }) => Promise<AuthMirrorImportResult>;
213
+ deleteLocalCandidate?: (candidateName: string, source: {
214
+ nodeId: string;
215
+ label?: string | null;
216
+ reason?: string | null;
217
+ }) => Promise<{
218
+ ok: boolean;
219
+ deleted: boolean;
220
+ reason?: string | null;
221
+ }>;
173
222
  isIdle: () => boolean;
174
223
  notify?: (event: AuthSyncNotification) => Promise<void>;
175
224
  }
@@ -201,6 +250,7 @@ export declare class CrossNodeAuthSync {
201
250
  private readonly peers;
202
251
  private readonly peerKeys;
203
252
  private readonly pendingImports;
253
+ private readonly pendingDeletes;
204
254
  private readonly pendingPulls;
205
255
  private readonly pendingLeases;
206
256
  private readonly pendingTests;
@@ -208,6 +258,7 @@ export declare class CrossNodeAuthSync {
208
258
  private lastPeerActivityAt;
209
259
  private timer;
210
260
  private importProcessorActive;
261
+ private deleteProcessorActive;
211
262
  private activeRemoteLease;
212
263
  private activeLocalLease;
213
264
  private lastNotifiedError;
@@ -223,6 +274,7 @@ export declare class CrossNodeAuthSync {
223
274
  sent: number;
224
275
  skipped: number;
225
276
  }>;
277
+ publishCandidateDeletion(candidateName: string, reason?: string | null): Promise<boolean>;
226
278
  publishDigest(): Promise<void>;
227
279
  requestRecovery(candidateName: string, current?: {
228
280
  accountId: string | null;
@@ -242,6 +294,9 @@ export declare class CrossNodeAuthSync {
242
294
  private handleLeaseReply;
243
295
  private enqueueImport;
244
296
  private processPendingImports;
297
+ private enqueueDelete;
298
+ private processPendingDeletes;
299
+ private deleteRemoteCandidate;
245
300
  private validateAndImport;
246
301
  private sendToAll;
247
302
  private sendToPeer;
@@ -21,6 +21,7 @@ export class CrossNodeAuthSync {
21
21
  peers;
22
22
  peerKeys;
23
23
  pendingImports = [];
24
+ pendingDeletes = [];
24
25
  pendingPulls = new Map();
25
26
  pendingLeases = new Map();
26
27
  pendingTests = new Map();
@@ -28,6 +29,7 @@ export class CrossNodeAuthSync {
28
29
  lastPeerActivityAt = new Map();
29
30
  timer = null;
30
31
  importProcessorActive = false;
32
+ deleteProcessorActive = false;
31
33
  activeRemoteLease = null;
32
34
  activeLocalLease = null;
33
35
  lastNotifiedError = null;
@@ -79,8 +81,8 @@ export class CrossNodeAuthSync {
79
81
  return;
80
82
  this.timer = setInterval(() => {
81
83
  this.expireLeases();
82
- void this.processPendingImports().catch((error) => {
83
- this.recordError(`pending import failed: ${formatError(error)}`);
84
+ void this.processPendingDeletes().then(() => this.processPendingImports()).catch((error) => {
85
+ this.recordError(`pending auth sync work failed: ${formatError(error)}`);
84
86
  });
85
87
  }, 5_000);
86
88
  this.timer.unref();
@@ -124,7 +126,9 @@ export class CrossNodeAuthSync {
124
126
  }
125
127
  isIdle() {
126
128
  return this.pendingImports.length === 0
129
+ && this.pendingDeletes.length === 0
127
130
  && !this.importProcessorActive
131
+ && !this.deleteProcessorActive
128
132
  && this.pendingPulls.size === 0
129
133
  && this.pendingLeases.size === 0
130
134
  && this.pendingTests.size === 0
@@ -205,6 +209,48 @@ export class CrossNodeAuthSync {
205
209
  throw error;
206
210
  }
207
211
  }
212
+ async publishCandidateDeletion(candidateName, reason = null) {
213
+ if (!this.isReady() || !isAuthCandidateName(candidateName))
214
+ return false;
215
+ const requestId = crypto.randomUUID();
216
+ try {
217
+ await this.sendToAll({
218
+ kind: 'delete.candidate',
219
+ requestId,
220
+ candidateName,
221
+ reason,
222
+ deletedAt: new Date().toISOString(),
223
+ });
224
+ this.recordEvent({
225
+ direction: 'local',
226
+ kind: 'delete.candidate',
227
+ stage: 'sent',
228
+ peer: null,
229
+ requestId,
230
+ candidateName,
231
+ detail: reason,
232
+ });
233
+ this.clearCandidateFailure(candidateName);
234
+ await this.writeState();
235
+ this.notify({ kind: 'candidate_delete_sent', candidateName, peers: [...this.peers], reason });
236
+ return true;
237
+ }
238
+ catch (error) {
239
+ const formatted = formatError(error);
240
+ this.recordError(`candidate delete publish failed for ${candidateName}: ${formatted}`, false);
241
+ this.recordEvent({
242
+ direction: 'local',
243
+ kind: 'delete.candidate',
244
+ stage: 'send_failed',
245
+ peer: null,
246
+ requestId,
247
+ candidateName,
248
+ detail: formatted,
249
+ });
250
+ this.notify({ kind: 'candidate_delete_failed', candidateName, peers: [...this.peers], reason: formatted });
251
+ throw error;
252
+ }
253
+ }
208
254
  async publishDigest() {
209
255
  if (!this.isReady())
210
256
  return;
@@ -455,6 +501,9 @@ export class CrossNodeAuthSync {
455
501
  case 'pull.response':
456
502
  await this.handlePullResponse(message, senderNodeId, sourceLabel, normalizePeerIdentity(peer));
457
503
  return;
504
+ case 'delete.candidate':
505
+ this.enqueueDelete(message, senderNodeId, sourceLabel, normalizePeerIdentity(peer));
506
+ return;
458
507
  case 'digest':
459
508
  await this.handleDigest(message, normalizePeerIdentity(peer));
460
509
  return;
@@ -717,6 +766,8 @@ export class CrossNodeAuthSync {
717
766
  async processPendingImports() {
718
767
  if (this.importProcessorActive)
719
768
  return;
769
+ if (this.pendingDeletes.length > 0 || this.deleteProcessorActive)
770
+ return;
720
771
  if (!this.callbacks.isIdle())
721
772
  return;
722
773
  this.importProcessorActive = true;
@@ -732,6 +783,150 @@ export class CrossNodeAuthSync {
732
783
  this.importProcessorActive = false;
733
784
  }
734
785
  }
786
+ enqueueDelete(message, sourceNodeId, sourceLabel, fromPeer) {
787
+ if (!isAuthCandidateName(message.candidateName))
788
+ return;
789
+ const queued = this.deleteProcessorActive
790
+ || this.importProcessorActive
791
+ || !this.callbacks.isIdle()
792
+ || this.pendingImports.length > 0
793
+ || this.pendingDeletes.length > 0;
794
+ for (let index = this.pendingImports.length - 1; index >= 0; index -= 1) {
795
+ if (this.pendingImports[index]?.bundle.candidateName === message.candidateName) {
796
+ this.pendingImports.splice(index, 1);
797
+ }
798
+ }
799
+ this.pendingDeletes.push({
800
+ candidateName: message.candidateName,
801
+ requestId: message.requestId,
802
+ sourceNodeId,
803
+ sourceLabel,
804
+ receivedAt: Date.now(),
805
+ fromPeer,
806
+ reason: message.reason ?? null,
807
+ });
808
+ this.recordEvent({
809
+ direction: 'local',
810
+ kind: 'delete.candidate',
811
+ stage: queued ? 'queued' : 'processing',
812
+ peer: fromPeer,
813
+ requestId: message.requestId,
814
+ candidateName: message.candidateName,
815
+ detail: message.reason ?? null,
816
+ });
817
+ this.notify({
818
+ kind: 'remote_delete_received',
819
+ candidateName: message.candidateName,
820
+ sourceNodeId,
821
+ sourceLabel: sourceLabel ?? fromPeer,
822
+ peer: fromPeer,
823
+ queued,
824
+ queueLength: this.pendingDeletes.length,
825
+ reason: message.reason ?? null,
826
+ });
827
+ void this.processPendingDeletes().then(() => this.processPendingImports()).catch((error) => {
828
+ this.recordError(`remote delete failed: ${formatError(error)}`);
829
+ });
830
+ }
831
+ async processPendingDeletes() {
832
+ if (this.deleteProcessorActive)
833
+ return;
834
+ if (!this.callbacks.isIdle())
835
+ return;
836
+ this.deleteProcessorActive = true;
837
+ try {
838
+ while (this.pendingDeletes.length > 0) {
839
+ if (!this.callbacks.isIdle())
840
+ return;
841
+ const pending = this.pendingDeletes.shift();
842
+ await this.deleteRemoteCandidate(pending);
843
+ }
844
+ }
845
+ finally {
846
+ this.deleteProcessorActive = false;
847
+ }
848
+ }
849
+ async deleteRemoteCandidate(pending) {
850
+ const source = pending.sourceLabel ?? pending.fromPeer;
851
+ if (!this.callbacks.deleteLocalCandidate) {
852
+ this.recordEvent({
853
+ direction: 'local',
854
+ kind: 'delete.candidate',
855
+ stage: 'skipped',
856
+ peer: pending.fromPeer,
857
+ requestId: pending.requestId,
858
+ candidateName: pending.candidateName,
859
+ detail: 'delete callback is not configured',
860
+ });
861
+ this.notify({
862
+ kind: 'remote_delete_skipped',
863
+ candidateName: pending.candidateName,
864
+ sourceNodeId: pending.sourceNodeId,
865
+ sourceLabel: source,
866
+ peer: pending.fromPeer,
867
+ reason: 'delete callback is not configured',
868
+ });
869
+ return;
870
+ }
871
+ const result = await this.callbacks.deleteLocalCandidate(pending.candidateName, {
872
+ nodeId: pending.sourceNodeId,
873
+ label: source,
874
+ reason: pending.reason,
875
+ });
876
+ if (!result.ok) {
877
+ const reason = result.reason ?? 'remote delete failed';
878
+ this.recordEvent({
879
+ direction: 'local',
880
+ kind: 'delete.candidate',
881
+ stage: 'failed',
882
+ peer: pending.fromPeer,
883
+ requestId: pending.requestId,
884
+ candidateName: pending.candidateName,
885
+ detail: reason,
886
+ });
887
+ this.notify({
888
+ kind: 'remote_delete_failed',
889
+ candidateName: pending.candidateName,
890
+ sourceNodeId: pending.sourceNodeId,
891
+ sourceLabel: source,
892
+ peer: pending.fromPeer,
893
+ reason,
894
+ });
895
+ return;
896
+ }
897
+ const clearedFailure = this.clearCandidateFailure(pending.candidateName);
898
+ this.recordEvent({
899
+ direction: 'local',
900
+ kind: 'delete.candidate',
901
+ stage: result.deleted ? 'deleted' : 'skipped',
902
+ peer: pending.fromPeer,
903
+ requestId: pending.requestId,
904
+ candidateName: pending.candidateName,
905
+ detail: result.reason ?? pending.reason,
906
+ });
907
+ if (clearedFailure) {
908
+ await this.writeState();
909
+ }
910
+ if (result.deleted) {
911
+ this.notify({
912
+ kind: 'remote_delete_deleted',
913
+ candidateName: pending.candidateName,
914
+ sourceNodeId: pending.sourceNodeId,
915
+ sourceLabel: source,
916
+ peer: pending.fromPeer,
917
+ reason: pending.reason,
918
+ });
919
+ return;
920
+ }
921
+ this.notify({
922
+ kind: 'remote_delete_skipped',
923
+ candidateName: pending.candidateName,
924
+ sourceNodeId: pending.sourceNodeId,
925
+ sourceLabel: source,
926
+ peer: pending.fromPeer,
927
+ reason: result.reason ?? 'candidate was already absent',
928
+ });
929
+ }
735
930
  async validateAndImport(bundle, sourceNodeId, sourceLabel, fromPeer, mode) {
736
931
  const source = sourceLabel ?? fromPeer;
737
932
  if (!isValidBundle(bundle)) {
@@ -1215,6 +1410,7 @@ function requestIdFromMessage(message) {
1215
1410
  return message.requestId ?? null;
1216
1411
  case 'pull.request':
1217
1412
  case 'pull.response':
1413
+ case 'delete.candidate':
1218
1414
  case 'test.ping':
1219
1415
  case 'test.pong':
1220
1416
  return message.requestId;
@@ -1232,6 +1428,7 @@ function candidateNameFromMessage(message) {
1232
1428
  switch (message.kind) {
1233
1429
  case 'push.bundle':
1234
1430
  case 'pull.request':
1431
+ case 'delete.candidate':
1235
1432
  return message.candidateName;
1236
1433
  case 'pull.response':
1237
1434
  return message.bundle?.candidateName ?? null;
package/dist/config.d.ts CHANGED
@@ -44,6 +44,7 @@ export interface AppConfig {
44
44
  statusPath: string;
45
45
  logPath: string;
46
46
  lockPath: string;
47
+ envPath: string | null;
47
48
  /** When true, start Weixin (iLink) long-poll alongside Telegram. */
48
49
  wxEnabled: boolean;
49
50
  /** Allowed `from_user_id` values for inbound Weixin messages (empty = allow any). */
@@ -61,6 +62,7 @@ export interface AppConfig {
61
62
  authSyncClusterId: string;
62
63
  authSyncStatePath: string;
63
64
  authSyncTempDir: string;
65
+ authAutoDeleteNeedsRepair: boolean;
64
66
  }
65
67
  export declare function loadConfig(): AppConfig;
66
68
  export declare function selectDefaultRuntimeBotToken(configuredTokens: string[], legacyToken: string | null): string | null;
package/dist/config.js CHANGED
@@ -81,6 +81,7 @@ export function loadConfig() {
81
81
  statusPath: DEFAULT_STATUS_PATH,
82
82
  logPath: DEFAULT_LOG_PATH,
83
83
  lockPath: process.env.LOCK_PATH || DEFAULT_LOCK_PATH,
84
+ envPath: getLoadedEnvPath(),
84
85
  wxEnabled: boolEnv('WX_ENABLED', false),
85
86
  wxAllowedIlinkUserIds: parseCommaSeparatedIds(process.env.WX_ALLOWED_ILINK_USER_IDS),
86
87
  weixinAccountsDir: process.env.WEIXIN_ACCOUNTS_DIR || path.join(APP_HOME, 'weixin', 'accounts'),
@@ -95,6 +96,7 @@ export function loadConfig() {
95
96
  authSyncClusterId: process.env.AUTH_SYNC_CLUSTER_ID?.trim() || 'default',
96
97
  authSyncStatePath: process.env.AUTH_SYNC_STATE_PATH || DEFAULT_AUTH_SYNC_STATE_PATH,
97
98
  authSyncTempDir: process.env.AUTH_SYNC_TEMP_DIR || DEFAULT_AUTH_SYNC_TEMP_DIR,
99
+ authAutoDeleteNeedsRepair: boolEnv('AUTH_AUTO_DELETE_NEEDS_REPAIR', false),
98
100
  };
99
101
  ensureAppDirs(config);
100
102
  return config;
@@ -9,7 +9,7 @@ import type { SelfUpdateRuntime, SelfUpdateStatus } from '../update.js';
9
9
  export interface CoreCoordinator {
10
10
  canSelfUpdate?: () => boolean;
11
11
  authCandidateUpdated?: (runtimeId: string, candidateName: string) => Promise<void>;
12
- authCandidateDeleted?: (runtimeId: string, candidateName: string) => Promise<void>;
12
+ authCandidateDeleted?: (runtimeId: string, candidateName: string, reason?: string | null) => Promise<void>;
13
13
  recoverAuthCandidate?: (runtimeId: string, candidateName: string, options?: {
14
14
  crossNode?: boolean;
15
15
  }) => Promise<boolean>;
@@ -195,6 +195,7 @@ export declare class BridgeSessionCore {
195
195
  private registerActiveTurn;
196
196
  private createActiveTurnState;
197
197
  getCurrentAuthLabel(): Promise<string | null>;
198
+ handleExternalCodexAuthCandidateDeleted(candidateName: string, reason?: string | null): Promise<void>;
198
199
  validateExternalCodexAuthCandidate(candidateName: string, rawAuth: string, expectedAccountId: string): Promise<{
199
200
  ok: boolean;
200
201
  reason?: string | null;
@@ -327,6 +328,9 @@ export declare class BridgeSessionCore {
327
328
  private handleAppsCommand;
328
329
  private handleFeaturesCommand;
329
330
  private handleConfigCommand;
331
+ private handleConfigToggleCallback;
332
+ private setAuthAutoDeleteNeedsRepair;
333
+ private formatConfigToggleUpdate;
330
334
  private handleRequirementsCommand;
331
335
  private handleProviderCommand;
332
336
  private handleMcpCommand;