@foxden-app/foxclaw 0.5.19 → 0.5.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
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.20 - 2026-06-08
6
+
7
+ ### 中文
8
+ - 修复跨节点 auth 同步远端导入验证和普通消息并发时的竞态:验证远端候选会临时重启 Codex app-server,现在这段窗口会标记为非空闲。
9
+ - 如果普通消息刚好在远端验证重启期间进入,FoxClaw 会提示稍后重发,不再把这条消息送进正在重启的 bridge 并报 `Codex app bridge stopped`。
10
+ - 普通对话启动过程现在也计入非空闲状态,避免 auth 同步验证插入到新 turn 建立中的窗口。
11
+
12
+ ### English
13
+ - Fixed a race between cross-node auth remote-import validation and ordinary messages. Remote candidate validation temporarily restarts Codex app-server, and that window is now marked non-idle.
14
+ - If an ordinary message arrives during the validation restart window, FoxClaw asks the user to resend it shortly instead of sending it into a restarting bridge and reporting `Codex app bridge stopped`.
15
+ - Starting an ordinary turn now also counts as non-idle, preventing auth sync validation from entering the small window while a new turn is being established.
16
+
5
17
  ## 0.5.19 - 2026-06-08
6
18
 
7
19
  ### 中文
@@ -70,6 +70,8 @@ export declare class BridgeSessionCore {
70
70
  private pendingAuthRotation;
71
71
  private authRotationInProgress;
72
72
  private authRefreshAllInProgress;
73
+ private externalAuthValidationInProgress;
74
+ private turnStartInProgress;
73
75
  private authRotationFailedTargets;
74
76
  private localUsageCache;
75
77
  private localUsageCacheLoaded;
@@ -119,6 +119,8 @@ export class BridgeSessionCore {
119
119
  pendingAuthRotation = null;
120
120
  authRotationInProgress = false;
121
121
  authRefreshAllInProgress = false;
122
+ externalAuthValidationInProgress = false;
123
+ turnStartInProgress = 0;
122
124
  authRotationFailedTargets = new Set();
123
125
  localUsageCache = null;
124
126
  localUsageCacheLoaded = false;
@@ -245,6 +247,8 @@ export class BridgeSessionCore {
245
247
  this.commandUsageSequence = 0;
246
248
  this.pendingAuthRotation = null;
247
249
  this.authRefreshAllInProgress = false;
250
+ this.externalAuthValidationInProgress = false;
251
+ this.turnStartInProgress = 0;
248
252
  this.clearObservedThreadWatchers();
249
253
  this.releaseActiveTurnsForBridgeShutdown();
250
254
  this.bot.stop();
@@ -311,6 +315,10 @@ export class BridgeSessionCore {
311
315
  await this.handleActiveTurnInboundMessage(event, locale, event.text.trim());
312
316
  return;
313
317
  }
318
+ if (this.externalAuthValidationInProgress) {
319
+ await this.sendMessage(scopeId, t(locale, 'auth_sync_validation_busy'));
320
+ return;
321
+ }
314
322
  await this.startBoundTurnFromEvent(event, locale, event.text.trim());
315
323
  return;
316
324
  }
@@ -365,6 +373,10 @@ export class BridgeSessionCore {
365
373
  await this.handleActiveTurnInboundMessage(event, locale, decision.text);
366
374
  return;
367
375
  }
376
+ if (this.externalAuthValidationInProgress) {
377
+ await this.sendMessage(scopeId, t(locale, 'auth_sync_validation_busy'));
378
+ return;
379
+ }
368
380
  await this.startBoundTurnFromEvent(event, locale, decision.text);
369
381
  }
370
382
  async handleCommand(event, locale, name, args) {
@@ -2920,52 +2932,61 @@ export class BridgeSessionCore {
2920
2932
  return (await this.listCodexAuthState()).currentLabel;
2921
2933
  }
2922
2934
  async validateExternalCodexAuthCandidate(candidateName, rawAuth, expectedAccountId) {
2923
- if (!this.isIdleForServiceUpdate()) {
2935
+ if (this.externalAuthValidationInProgress) {
2924
2936
  return { ok: false, reason: 'runtime is not idle' };
2925
2937
  }
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
- }
2938
+ if (!this.isIdleForServiceUpdate()) {
2939
+ return { ok: false, reason: 'runtime is not idle' };
2937
2940
  }
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`);
2941
+ this.externalAuthValidationInProgress = true;
2943
2942
  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' };
2943
+ const metadata = parseChatGptAuthMetadata(rawAuth);
2944
+ if (!metadata || metadata.accountId !== expectedAccountId) {
2945
+ return { ok: false, reason: 'remote auth account id mismatch' };
2946
+ }
2947
+ const state = await this.listCodexAuthState();
2948
+ const existing = state.candidates.find(candidate => candidate.name === candidateName) ?? null;
2949
+ if (existing) {
2950
+ const existingMetadata = await readChatGptAuthMetadata(existing.path);
2951
+ if (existingMetadata && existingMetadata.accountId !== expectedAccountId) {
2952
+ return { ok: false, reason: 'same candidate belongs to a different account' };
2953
+ }
2954
+ }
2955
+ const authStat = await fs.lstat(state.authPath).catch(() => null);
2956
+ const originalRegularAuth = authStat?.isFile()
2957
+ ? await fs.readFile(state.authPath, 'utf8').catch(() => null)
2958
+ : null;
2959
+ const tempPath = path.join(state.authDir, `.auth-sync-validate-${process.pid}-${Date.now()}.json`);
2960
+ try {
2961
+ await fs.writeFile(tempPath, rawAuth, { encoding: 'utf8', mode: 0o600 });
2962
+ await pointCodexAuthAtTarget(state.authDir, state.authPath, tempPath);
2963
+ this.pendingTurnErrors.clear();
2964
+ this.attachedThreads.clear();
2965
+ await this.app.restart();
2966
+ const account = await this.app.readAccount(false);
2967
+ const rateLimits = await this.app.readAccountRateLimits();
2968
+ if (!account || !rateLimits || !selectCodexRateLimitSnapshot(rateLimits)) {
2969
+ return { ok: false, reason: 'Codex did not validate ChatGPT usage for remote auth' };
2970
+ }
2971
+ return { ok: true };
2972
+ }
2973
+ catch (error) {
2974
+ return { ok: false, reason: formatUserError(error) };
2975
+ }
2976
+ finally {
2977
+ await restoreCodexAuthTarget(state.authDir, state.authPath, state.currentTargetPath, originalRegularAuth).catch((error) => {
2978
+ this.logger.warn('codex.auth_sync_restore_failed', { error: toErrorMeta(error) });
2979
+ });
2980
+ await fs.rm(tempPath, { force: true }).catch(() => undefined);
2981
+ this.pendingTurnErrors.clear();
2982
+ this.attachedThreads.clear();
2983
+ await this.app.restart().catch((error) => {
2984
+ this.logger.warn('codex.auth_sync_restart_restore_failed', { error: toErrorMeta(error) });
2985
+ });
2953
2986
  }
2954
- return { ok: true };
2955
- }
2956
- catch (error) {
2957
- return { ok: false, reason: formatUserError(error) };
2958
2987
  }
2959
2988
  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
- });
2989
+ this.externalAuthValidationInProgress = false;
2969
2990
  }
2970
2991
  }
2971
2992
  isIdleForServiceUpdate() {
@@ -2975,7 +2996,9 @@ export class BridgeSessionCore {
2975
2996
  && this.pendingMcpElicitations.size === 0
2976
2997
  && this.pendingLoginsByScope.size === 0
2977
2998
  && !this.authRotationInProgress
2978
- && !this.authRefreshAllInProgress;
2999
+ && !this.authRefreshAllInProgress
3000
+ && !this.externalAuthValidationInProgress
3001
+ && this.turnStartInProgress === 0;
2979
3002
  }
2980
3003
  hasLocalBlockingActivity() {
2981
3004
  return !this.isIdleForServiceUpdate();
@@ -6913,36 +6936,46 @@ export class BridgeSessionCore {
6913
6936
  }
6914
6937
  async startBoundTurnFromEvent(event, locale, text) {
6915
6938
  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;
6939
+ if (this.externalAuthValidationInProgress) {
6940
+ await this.sendMessage(scopeId, t(locale, 'auth_sync_validation_busy'));
6941
+ return;
6942
+ }
6943
+ this.turnStartInProgress += 1;
6924
6944
  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);
6945
+ this.clearPlanImplementationPromptsForScope(scopeId);
6946
+ await this.stopWatchingScopeThread(scopeId);
6947
+ const existingBinding = this.store.getBinding(scopeId);
6948
+ const binding = existingBinding
6949
+ ? await this.ensureThreadReady(scopeId, existingBinding)
6950
+ : await this.createBinding(scopeId, null);
6951
+ await this.sendTyping(scopeId);
6952
+ const previewMessageId = 0;
6953
+ try {
6954
+ const input = await this.buildTurnInput(binding, { ...event, text }, locale);
6955
+ const turnState = await this.startTurnWithRecovery(scopeId, binding, input);
6956
+ if (turnState.collaborationMode === 'plan') {
6957
+ this.store.setChatCollaborationMode(scopeId, DEFAULT_COLLABORATION_MODE);
6958
+ }
6959
+ await this.registerActiveTurn(scopeId, event.chatId, event.chatType, event.topicId, turnState.threadId, turnState.turnId, previewMessageId, {
6960
+ input,
6961
+ threadId: turnState.threadId,
6962
+ cwd: this.store.getBinding(scopeId)?.cwd ?? binding.cwd ?? this.config.defaultCwd,
6963
+ chatId: event.chatId,
6964
+ chatType: event.chatType,
6965
+ topicId: event.topicId,
6966
+ collaborationMode: turnState.collaborationMode,
6967
+ failedAuthTargets: new Set(),
6968
+ }, turnState.collaborationMode);
6929
6969
  }
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);
6970
+ catch (error) {
6971
+ if (previewMessageId > 0) {
6972
+ await this.cleanupTransientPreview(scopeId, previewMessageId);
6973
+ }
6974
+ throw error;
6944
6975
  }
6945
- throw error;
6976
+ }
6977
+ finally {
6978
+ this.turnStartInProgress -= 1;
6946
6979
  }
6947
6980
  }
6948
6981
  async requestInterrupt(active) {
package/dist/i18n.d.ts CHANGED
@@ -225,6 +225,7 @@ declare const MESSAGES: {
225
225
  readonly auth_add_reverted: "Restored previous auth.";
226
226
  readonly auth_add_missing_file: "Login completed, but the new auth file was not created: {value}";
227
227
  readonly auth_sync_disabled: "Cross-node auth sync is disabled.";
228
+ 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
229
  readonly auth_sync_test_sent: "Auth sync test complete: sent {sent}, replies {replied}.";
229
230
  readonly auth_sync_test_missing: "Missing replies: {value}";
230
231
  readonly auth_sync_push_blocked_active: "Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.";
@@ -899,6 +900,7 @@ declare const MESSAGES: {
899
900
  readonly auth_add_reverted: "已恢复之前的 auth。";
900
901
  readonly auth_add_missing_file: "登录已完成,但没有创建新的 auth 文件:{value}";
901
902
  readonly auth_sync_disabled: "跨节点 auth 同步未启用。";
903
+ readonly auth_sync_validation_busy: "正在验证跨节点 auth,并临时重启 Codex app-server。请稍等片刻后再发送这条消息。";
902
904
  readonly auth_sync_test_sent: "auth sync 测试完成:已发送 {sent},收到回应 {replied}。";
903
905
  readonly auth_sync_test_missing: "未回应:{value}";
904
906
  readonly auth_sync_push_blocked_active: "当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。";
package/dist/i18n.js CHANGED
@@ -223,6 +223,7 @@ const MESSAGES = {
223
223
  auth_add_reverted: 'Restored previous auth.',
224
224
  auth_add_missing_file: 'Login completed, but the new auth file was not created: {value}',
225
225
  auth_sync_disabled: 'Cross-node auth sync is disabled.',
226
+ 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
227
  auth_sync_test_sent: 'Auth sync test complete: sent {sent}, replies {replied}.',
227
228
  auth_sync_test_missing: 'Missing replies: {value}',
228
229
  auth_sync_push_blocked_active: 'Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.',
@@ -897,6 +898,7 @@ const MESSAGES = {
897
898
  auth_add_reverted: '已恢复之前的 auth。',
898
899
  auth_add_missing_file: '登录已完成,但没有创建新的 auth 文件:{value}',
899
900
  auth_sync_disabled: '跨节点 auth 同步未启用。',
901
+ auth_sync_validation_busy: '正在验证跨节点 auth,并临时重启 Codex app-server。请稍等片刻后再发送这条消息。',
900
902
  auth_sync_test_sent: 'auth sync 测试完成:已发送 {sent},收到回应 {replied}。',
901
903
  auth_sync_test_missing: '未回应:{value}',
902
904
  auth_sync_push_blocked_active: '当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。',
@@ -166,7 +166,7 @@ Confirm that pending imports were processed, or that the candidate exists or has
166
166
 
167
167
  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
168
 
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.
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. 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.
170
170
 
171
171
  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
172
 
@@ -425,7 +425,7 @@ OpenAI does not publish a fixed ChatGPT refresh-token lifetime or an old-token r
425
425
 
426
426
  ### 6.4 Cross-Node Auth Sync
427
427
 
428
- Cross-node auth sync is disabled by default. It is for multiple machines you control that share the same legally owned ChatGPT auth candidate pool, so a token refreshed by Codex on one node can be copied to the others. v1 uses Telegram Bot-to-Bot private messages to carry encrypted files, so it does not require public IPs or FRP. The recommended default is one contact bot per node; other bots on the same node keep using local auth mirroring. In multi-bot mode, the default contact is the first token in `TG_BOT_TOKENS`. The contact bot private chat reports send, receive, queue, import, failure, recovery-query, and manual-intervention states; refresh/send/import bursts are grouped into summaries, while recovery and manual-intervention notices remain explicit. Per-candidate validation failures are shown as candidate failures instead of overwriting the sync-system last error. Recent bot-to-bot traffic is also kept in an event ring so `/auth sync events [filter]` and `/auth sync trace <requestId>` can explain a specific candidate, peer, or request.
428
+ Cross-node auth sync is disabled by default. It is for multiple machines you control that share the same legally owned ChatGPT auth candidate pool, so a token refreshed by Codex on one node can be copied to the others. v1 uses Telegram Bot-to-Bot private messages to carry encrypted files, so it does not require public IPs or FRP. The recommended default is one contact bot per node; other bots on the same node keep using local auth mirroring. In multi-bot mode, the default contact is the first token in `TG_BOT_TOKENS`. The contact bot private chat reports send, receive, queue, import, failure, recovery-query, and manual-intervention states; refresh/send/import bursts are grouped into summaries, while recovery and manual-intervention notices remain explicit. Remote import validation temporarily restarts the local Codex app-server; FoxClaw marks that window non-idle, and ordinary messages received during it get a short retry notice instead of running against a restarting bridge. Per-candidate validation failures are shown as candidate failures instead of overwriting the sync-system last error. Recent bot-to-bot traffic is also kept in an event ring so `/auth sync events [filter]` and `/auth sync trace <requestId>` can explain a specific candidate, peer, or request.
429
429
 
430
430
  For the full design, safety boundaries, `.env` examples, and troubleshooting, read the [Cross-Node Auth Sync Setup Guide](./cross-node-auth-sync.md).
431
431
 
@@ -166,7 +166,7 @@ auth sync 测试完成:已发送 1,收到回应 1。
166
166
 
167
167
  注意:`/auth sync push all` 的“已发送”只代表本节点把加密包发给 Telegram 成功,不代表对端已经写盘。对端只有在全局空闲、usage 验证通过、同名候选 account id 一致且 ChatGPT 用户/邮箱身份兼容,并且远端 `last_refresh` 比本地更新时才会覆盖文件。如果本地已经是相同或更新版本,文件不会变化,`最近导入` 也可能保持为空。
168
168
 
169
- 启用跨节点同步后,联系人 bot 的私聊会收到节点级通知:本机 auth 更新并开始发往哪些 peer、收到远端包后是排队还是立即验证、导入成功/跳过/失败原因、auth 恢复时正在查询哪些 peer、peer 回应了什么,以及所有 peer 都无法提供可用副本时的人工介入提示。刷新、发送、导入密集发生时会合并成简短汇总,避免一个候选更新拆成开始、收到、镜像写入和完成多条消息;恢复和人工介入提示仍会明确发出。通知不会包含 auth 内容、token 或同步密文。
169
+ 启用跨节点同步后,联系人 bot 的私聊会收到节点级通知:本机 auth 更新并开始发往哪些 peer、收到远端包后是排队还是立即验证、导入成功/跳过/失败原因、auth 恢复时正在查询哪些 peer、peer 回应了什么,以及所有 peer 都无法提供可用副本时的人工介入提示。刷新、发送、导入密集发生时会合并成简短汇总,避免一个候选更新拆成开始、收到、镜像写入和完成多条消息;恢复和人工介入提示仍会明确发出。远端导入验证会临时重启本机 Codex app-server;这段窗口里 FoxClaw 会把 runtime 视为非空闲,并让普通消息稍后重发,而不是送进正在重启的 bridge。通知不会包含 auth 内容、token 或同步密文。
170
170
 
171
171
  从 0.5.2 起,`/auth sync status` 会把同步系统级 `最近错误` 和单个 auth 的 `候选失败` 分开显示。比如某个远端候选返回 `token_invalidated` 或 access token 过期时,只会记录到该候选名下面;当前 `auth.json` 是否健康仍以当前 auth 的 usage 验证为准。`local candidate is already newer or equal` 属于正常跳过,不会记为错误。
172
172
 
@@ -425,7 +425,7 @@ OpenAI 没有公开 ChatGPT refresh token 的固定有效期或旧 token 重放
425
425
 
426
426
  ### 6.4 跨节点 auth 同步
427
427
 
428
- 跨节点 auth 同步默认关闭。它适合你在多台自己控制的机器上使用同一组合法 ChatGPT 账号候选,并希望某台机器上 Codex 自动刷新出的新 token 能同步到其他机器。v1 使用 Telegram Bot-to-Bot 私聊传输加密文件,不需要公网 IP 或 FRP。推荐每台机器选择一个联系人 bot;同一节点内其他 bot 继续使用本机 auth 镜像。多 bot 模式下,默认联系人是 `TG_BOT_TOKENS` 的第一个 token。联系人 bot 的私聊会报告发送、接收、排队、导入、失败、恢复查询和人工介入提示;刷新、发送、导入密集发生时会合并成汇总,恢复和人工介入提示仍会明确发出。单个候选验证失败会作为“候选失败”显示,不会覆盖同步系统级最近错误。最近 bot-to-bot 通讯会保存在事件环里,可用 `/auth sync events [过滤]` 和 `/auth sync trace <requestId>` 追查某个候选、peer 或请求。
428
+ 跨节点 auth 同步默认关闭。它适合你在多台自己控制的机器上使用同一组合法 ChatGPT 账号候选,并希望某台机器上 Codex 自动刷新出的新 token 能同步到其他机器。v1 使用 Telegram Bot-to-Bot 私聊传输加密文件,不需要公网 IP 或 FRP。推荐每台机器选择一个联系人 bot;同一节点内其他 bot 继续使用本机 auth 镜像。多 bot 模式下,默认联系人是 `TG_BOT_TOKENS` 的第一个 token。联系人 bot 的私聊会报告发送、接收、排队、导入、失败、恢复查询和人工介入提示;刷新、发送、导入密集发生时会合并成汇总,恢复和人工介入提示仍会明确发出。远端导入验证会临时重启本机 Codex app-server;FoxClaw 会把这段窗口标记为非空闲,期间收到的普通消息会收到稍后重发提示,不再进入正在重启的 bridge。单个候选验证失败会作为“候选失败”显示,不会覆盖同步系统级最近错误。最近 bot-to-bot 通讯会保存在事件环里,可用 `/auth sync events [过滤]` 和 `/auth sync trace <requestId>` 追查某个候选、peer 或请求。
429
429
 
430
430
  完整设计、安全边界、`.env` 示例和排查步骤见 [跨节点 auth 同步配置指南](./cross-node-auth-sync.md)。
431
431
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.5.19",
3
+ "version": "0.5.20",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",