@foxden-app/foxclaw 0.3.17 → 0.3.19

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.
Files changed (42) hide show
  1. package/.env.example +5 -2
  2. package/README.md +14 -4
  3. package/README_EN.md +14 -4
  4. package/dist/auth/mirror.d.ts +25 -0
  5. package/dist/auth/mirror.js +199 -0
  6. package/dist/codex_app/client.d.ts +2 -1
  7. package/dist/codex_app/client.js +10 -2
  8. package/dist/codex_app/local_usage.d.ts +7 -6
  9. package/dist/codex_app/local_usage.js +55 -66
  10. package/dist/config.d.ts +7 -0
  11. package/dist/config.js +18 -1
  12. package/dist/controller/controller.d.ts +14 -2
  13. package/dist/controller/controller.js +110 -34
  14. package/dist/core/bridge_scope.d.ts +5 -2
  15. package/dist/core/bridge_scope.js +7 -3
  16. package/dist/i18n.d.ts +4 -4
  17. package/dist/i18n.js +4 -4
  18. package/dist/main.js +144 -9
  19. package/dist/store/database.d.ts +4 -2
  20. package/dist/store/database.js +42 -6
  21. package/dist/telegram/addressing.d.ts +1 -0
  22. package/dist/telegram/addressing.js +3 -0
  23. package/dist/telegram/gateway.d.ts +4 -1
  24. package/dist/telegram/gateway.js +23 -5
  25. package/dist/types.d.ts +7 -0
  26. package/dist/update.d.ts +5 -0
  27. package/dist/update.js +93 -5
  28. package/docs/agent-assisted-install.md +4 -4
  29. package/docs/install-for-beginners.md +3 -3
  30. package/docs/troubleshooting.md +1 -1
  31. package/docs/user-manual.md +11 -11
  32. package/docs/zh/agent-assisted-install.md +4 -4
  33. package/docs/zh/foxclaw-skill.md +1 -1
  34. package/docs/zh/install-for-beginners.md +3 -3
  35. package/docs/zh/troubleshooting.md +1 -1
  36. package/docs/zh/user-manual.md +11 -11
  37. package/package.json +1 -1
  38. package/skills/foxclaw/SKILL.md +26 -19
  39. package/skills/foxclaw/references/telegram-setup.md +5 -4
  40. package/skills/foxclaw/scripts/bootstrap_host.py +11 -8
  41. package/skills/foxclaw/scripts/bootstrap_remote.py +8 -4
  42. package/skills/npm-publish/SKILL.md +3 -0
package/dist/config.d.ts CHANGED
@@ -7,12 +7,17 @@ export declare const DEFAULT_LOG_PATH: string;
7
7
  export declare const DEFAULT_LOCK_PATH: string;
8
8
  export declare const DEFAULT_CODEX_APP_SERVER_STATE_PATH: string;
9
9
  export declare const DEFAULT_CODEX_APP_SERVER_LOG_PATH: string;
10
+ export declare const DEFAULT_CODEX_TELEGRAM_HOME: string;
10
11
  export declare const DEFAULT_ENV_PATH: string;
11
12
  export declare function resolveEnvPath(): string;
12
13
  export declare function getLoadedEnvPath(): string | null;
13
14
  export declare function loadEnv(): void;
14
15
  export interface AppConfig {
15
16
  tgBotToken: string;
17
+ tgBotTokens: string[];
18
+ tgMultiBotMode: boolean;
19
+ tgScopeBotId: string | null;
20
+ tgRequireExplicitGroupAddressing: boolean;
16
21
  tgAllowedUserId: string;
17
22
  tgAllowedChatId: string | null;
18
23
  tgAllowedTopicId: number | null;
@@ -21,6 +26,8 @@ export interface AppConfig {
21
26
  codexAppLaunchCmd: string;
22
27
  codexAppServerStatePath: string;
23
28
  codexAppServerLogPath: string;
29
+ codexAuthDir: string | null;
30
+ codexHome: string | null;
24
31
  codexAppSyncOnOpen: boolean;
25
32
  codexAppSyncOnTurnComplete: boolean;
26
33
  storePath: string;
package/dist/config.js CHANGED
@@ -10,6 +10,7 @@ export const DEFAULT_LOG_PATH = path.join(APP_HOME, 'logs', 'service.log');
10
10
  export const DEFAULT_LOCK_PATH = path.join(APP_HOME, 'runtime', 'bridge.lock');
11
11
  export const DEFAULT_CODEX_APP_SERVER_STATE_PATH = path.join(APP_HOME, 'runtime', 'codex-app-server.json');
12
12
  export const DEFAULT_CODEX_APP_SERVER_LOG_PATH = path.join(APP_HOME, 'logs', 'codex-app-server.log');
13
+ export const DEFAULT_CODEX_TELEGRAM_HOME = path.join(APP_HOME, 'codex', 'telegram');
13
14
  export const DEFAULT_ENV_PATH = path.join(APP_HOME, '.env');
14
15
  let envLoaded = false;
15
16
  let loadedEnvPath = null;
@@ -37,8 +38,22 @@ export function loadEnv() {
37
38
  }
38
39
  export function loadConfig() {
39
40
  loadEnv();
41
+ const configuredTokens = parseCommaSeparatedIds(process.env.TG_BOT_TOKENS);
42
+ const legacyToken = optional('TG_BOT_TOKEN');
43
+ const tgBotTokens = configuredTokens.length > 0
44
+ ? configuredTokens
45
+ : legacyToken
46
+ ? [legacyToken]
47
+ : [];
48
+ if (tgBotTokens.length === 0) {
49
+ throw new Error('TG_BOT_TOKENS or TG_BOT_TOKEN is required');
50
+ }
40
51
  const config = {
41
- tgBotToken: required('TG_BOT_TOKEN'),
52
+ tgBotToken: tgBotTokens[0],
53
+ tgBotTokens,
54
+ tgMultiBotMode: configuredTokens.length > 0,
55
+ tgScopeBotId: null,
56
+ tgRequireExplicitGroupAddressing: configuredTokens.length > 1,
42
57
  tgAllowedUserId: required('TG_ALLOWED_USER_ID'),
43
58
  tgAllowedChatId: optional('TG_ALLOWED_CHAT_ID'),
44
59
  tgAllowedTopicId: nullableIntEnv('TG_ALLOWED_TOPIC_ID'),
@@ -47,6 +62,8 @@ export function loadConfig() {
47
62
  codexAppLaunchCmd: process.env.CODEX_APP_LAUNCH_CMD || 'codex app',
48
63
  codexAppServerStatePath: process.env.CODEX_APP_SERVER_STATE_PATH || DEFAULT_CODEX_APP_SERVER_STATE_PATH,
49
64
  codexAppServerLogPath: process.env.CODEX_APP_SERVER_LOG_PATH || DEFAULT_CODEX_APP_SERVER_LOG_PATH,
65
+ codexAuthDir: process.env.CODEX_AUTH_DIR?.trim() || null,
66
+ codexHome: process.env.CODEX_HOME?.trim() || null,
50
67
  codexAppSyncOnOpen: boolEnv('CODEX_APP_SYNC_ON_OPEN', true),
51
68
  codexAppSyncOnTurnComplete: boolEnv('CODEX_APP_SYNC_ON_TURN_COMPLETE', false),
52
69
  storePath: process.env.STORE_PATH || DEFAULT_STORE_PATH,
@@ -6,6 +6,11 @@ import type { TelegramGateway, TelegramTextEvent } from '../telegram/gateway.js'
6
6
  import { BridgeMessagingRouter } from '../channels/bridge_messaging_router.js';
7
7
  import type { CodexAppClient } from '../codex_app/client.js';
8
8
  import type { SelfUpdateRuntime } from '../update.js';
9
+ export interface CoreCoordinator {
10
+ canSelfUpdate?: () => boolean;
11
+ authCandidateUpdated?: (runtimeId: string, candidateName: string) => Promise<void>;
12
+ statusUpdated?: (status: RuntimeStatus) => void;
13
+ }
9
14
  export declare class BridgeSessionCore {
10
15
  private readonly config;
11
16
  private readonly store;
@@ -13,6 +18,7 @@ export declare class BridgeSessionCore {
13
18
  private readonly bot;
14
19
  private readonly app;
15
20
  private readonly selfUpdater;
21
+ private readonly coordinator;
16
22
  private activeTurns;
17
23
  private activeTurnsByTurnId;
18
24
  private observedThreadWatchers;
@@ -51,7 +57,7 @@ export declare class BridgeSessionCore {
51
57
  /** Last threads-panel pagination state per scope (Telegram inline nav + /open index alignment). */
52
58
  private threadListPresentationState;
53
59
  private readonly messaging;
54
- constructor(config: AppConfig, store: BridgeStore, logger: Logger, bot: TelegramGateway, app: CodexAppClient, outbound: BridgeMessagingRouter, selfUpdater?: SelfUpdateRuntime | null);
60
+ constructor(config: AppConfig, store: BridgeStore, logger: Logger, bot: TelegramGateway, app: CodexAppClient, outbound: BridgeMessagingRouter, selfUpdater?: SelfUpdateRuntime | null, coordinator?: CoreCoordinator | null);
55
61
  /** Wire Telegram inbound events. Call before {@link startCodexApp}. */
56
62
  registerTelegramInboundHandlers(): void;
57
63
  /**
@@ -140,6 +146,10 @@ export declare class BridgeSessionCore {
140
146
  private stageAttachments;
141
147
  private registerActiveTurn;
142
148
  private createActiveTurnState;
149
+ isIdleForServiceUpdate(): boolean;
150
+ private hasLocalBlockingActivity;
151
+ private authRuntimeId;
152
+ private ownsScope;
143
153
  private setActiveTurn;
144
154
  private getActiveTurn;
145
155
  private getActiveTurnsForTurn;
@@ -261,13 +271,14 @@ export declare class BridgeSessionCore {
261
271
  private retryTurnAfterAuthRotation;
262
272
  private selectNextCodexAuthCandidate;
263
273
  private listCodexAuthState;
274
+ private resolveAuthDir;
264
275
  private readCodexAuthSwitchLabels;
265
276
  private codexAuthSwitchParams;
266
277
  private switchCodexAuthAndRestart;
267
278
  private buildNativeCollaborationMode;
268
279
  private buildCodexUsageStatusLines;
269
280
  private buildCodexLocalUsageStatusLines;
270
- private formatCodexLocalOutputSpeedStatusLines;
281
+ private formatCodexLocalResponseThroughputStatusLines;
271
282
  private resolveFastStatusLabel;
272
283
  private readCachedCodexLocalUsageStats;
273
284
  private refreshCodexLocalUsageIfNeeded;
@@ -277,6 +288,7 @@ export declare class BridgeSessionCore {
277
288
  private readCodexAuthQuotaSnapshots;
278
289
  private writeCodexAuthQuotaSnapshots;
279
290
  private codexAuthQuotaSnapshotPath;
291
+ private runtimeSnapshotFilename;
280
292
  private sendThreadContextSummary;
281
293
  private handleModelCommand;
282
294
  private handleEffortCommand;
@@ -93,6 +93,7 @@ export class BridgeSessionCore {
93
93
  bot;
94
94
  app;
95
95
  selfUpdater;
96
+ coordinator;
96
97
  activeTurns = new Map();
97
98
  activeTurnsByTurnId = new Map();
98
99
  observedThreadWatchers = new Map();
@@ -131,13 +132,14 @@ export class BridgeSessionCore {
131
132
  /** Last threads-panel pagination state per scope (Telegram inline nav + /open index alignment). */
132
133
  threadListPresentationState = new Map();
133
134
  messaging;
134
- constructor(config, store, logger, bot, app, outbound, selfUpdater = null) {
135
+ constructor(config, store, logger, bot, app, outbound, selfUpdater = null, coordinator = null) {
135
136
  this.config = config;
136
137
  this.store = store;
137
138
  this.logger = logger;
138
139
  this.bot = bot;
139
140
  this.app = app;
140
141
  this.selfUpdater = selfUpdater;
142
+ this.coordinator = coordinator;
141
143
  this.messaging = outbound;
142
144
  }
143
145
  /** Wire Telegram inbound events. Call before {@link startCodexApp}. */
@@ -323,6 +325,7 @@ export class BridgeSessionCore {
323
325
  allowedChatId: this.config.tgAllowedChatId,
324
326
  allowedTopicId: this.config.tgAllowedTopicId,
325
327
  topicId: event.topicId,
328
+ requireExplicitGroupAddressing: this.config.tgRequireExplicitGroupAddressing,
326
329
  }),
327
330
  replyToBot: event.replyToBot,
328
331
  });
@@ -1563,6 +1566,16 @@ export class BridgeSessionCore {
1563
1566
  return;
1564
1567
  }
1565
1568
  const lines = [t(locale, 'auth_add_done', { value: pendingAuthAdd.name })];
1569
+ try {
1570
+ await this.coordinator?.authCandidateUpdated?.(this.authRuntimeId(), pendingAuthAdd.name);
1571
+ }
1572
+ catch (error) {
1573
+ this.logger.warn('codex.auth_candidate_sync_failed', {
1574
+ candidate: pendingAuthAdd.name,
1575
+ runtimeId: this.authRuntimeId(),
1576
+ error: toErrorMeta(error),
1577
+ });
1578
+ }
1566
1579
  lines.push(...await this.buildCodexUsageStatusLines(locale));
1567
1580
  await this.sendMessage(scopeId, lines.join('\n'));
1568
1581
  return;
@@ -1572,7 +1585,7 @@ export class BridgeSessionCore {
1572
1585
  : t(locale, 'login_failed', { error: params?.error ?? t(locale, 'unknown') }));
1573
1586
  }
1574
1587
  async restorePendingAuthAdd(record) {
1575
- const state = await listCodexAuthState();
1588
+ const state = await this.listCodexAuthState();
1576
1589
  await this.restoreAuthAfterAddFailure(state.authDir, state.authPath, record.previousTargetPath);
1577
1590
  }
1578
1591
  async restoreAuthAfterAddFailure(authDir, authPath, previousTargetPath) {
@@ -1849,6 +1862,9 @@ export class BridgeSessionCore {
1849
1862
  }
1850
1863
  async restorePendingUserInputs() {
1851
1864
  for (const stored of this.store.listPendingUserInputs()) {
1865
+ if (!this.ownsScope(stored.chatId)) {
1866
+ continue;
1867
+ }
1852
1868
  const record = parseStoredPendingUserInput(stored);
1853
1869
  if (!record) {
1854
1870
  this.store.markPendingUserInputResolved(stored.localId);
@@ -2535,6 +2551,34 @@ export class BridgeSessionCore {
2535
2551
  resolver,
2536
2552
  };
2537
2553
  }
2554
+ isIdleForServiceUpdate() {
2555
+ return this.activeTurns.size === 0
2556
+ && this.pendingApprovalMessages.size === 0
2557
+ && this.pendingUserInputs.size === 0
2558
+ && this.pendingMcpElicitations.size === 0
2559
+ && this.pendingLoginsByScope.size === 0
2560
+ && !this.authRotationInProgress;
2561
+ }
2562
+ hasLocalBlockingActivity() {
2563
+ return !this.isIdleForServiceUpdate();
2564
+ }
2565
+ authRuntimeId() {
2566
+ return this.config.tgScopeBotId ?? 'default';
2567
+ }
2568
+ ownsScope(scopeId) {
2569
+ if (scopeId.startsWith(BRIDGE_SCOPE_WEIXIN_PREFIX)) {
2570
+ return this.messaging.hasWeixinTransport;
2571
+ }
2572
+ if (!this.config.tgScopeBotId) {
2573
+ return parseTelegramTargetFromBridgeScope(scopeId).botId === null;
2574
+ }
2575
+ try {
2576
+ return parseTelegramTargetFromBridgeScope(scopeId).botId === this.config.tgScopeBotId;
2577
+ }
2578
+ catch {
2579
+ return false;
2580
+ }
2581
+ }
2538
2582
  setActiveTurn(scopeId, turnId, active) {
2539
2583
  const key = activeTurnKey(scopeId, turnId);
2540
2584
  this.activeTurns.set(key, active);
@@ -2785,7 +2829,7 @@ export class BridgeSessionCore {
2785
2829
  }
2786
2830
  }
2787
2831
  for (const scopeId of this.store.findAllChatIdsByThreadId(threadId)) {
2788
- if (!this.messaging.canSendToScope(scopeId)) {
2832
+ if (!this.ownsScope(scopeId) || !this.messaging.canSendToScope(scopeId)) {
2789
2833
  continue;
2790
2834
  }
2791
2835
  scopes.add(scopeId);
@@ -2818,7 +2862,12 @@ export class BridgeSessionCore {
2818
2862
  return next;
2819
2863
  }
2820
2864
  updateStatus() {
2821
- writeRuntimeStatus(this.config.statusPath, this.getRuntimeStatus());
2865
+ const status = this.getRuntimeStatus();
2866
+ if (this.coordinator?.statusUpdated) {
2867
+ this.coordinator.statusUpdated(status);
2868
+ return;
2869
+ }
2870
+ writeRuntimeStatus(this.config.statusPath, status);
2822
2871
  }
2823
2872
  async sendMessage(scopeId, text, inlineKeyboard) {
2824
2873
  return this.messaging.sendPlain(scopeId, text, inlineKeyboard);
@@ -3653,7 +3702,7 @@ export class BridgeSessionCore {
3653
3702
  ].join('\n'));
3654
3703
  }
3655
3704
  async handleAuthReloadCommand(scopeId, locale) {
3656
- if (this.activeTurns.size > 0 || this.store.countPendingApprovals() > 0 || this.pendingUserInputs.size > 0 || this.pendingMcpElicitations.size > 0) {
3705
+ if (this.hasLocalBlockingActivity()) {
3657
3706
  await this.sendMessage(scopeId, t(locale, 'auth_reload_blocked_active'));
3658
3707
  return;
3659
3708
  }
@@ -3670,7 +3719,7 @@ export class BridgeSessionCore {
3670
3719
  await this.sendMessage(scopeId, t(locale, 'update_unavailable'));
3671
3720
  return;
3672
3721
  }
3673
- if (this.activeTurns.size > 0 || this.store.countPendingApprovals() > 0 || this.pendingUserInputs.size > 0 || this.pendingMcpElicitations.size > 0) {
3722
+ if (!this.isIdleForServiceUpdate() || this.store.countPendingApprovals() > 0 || this.store.countPendingUserInputs() > 0 || (this.coordinator?.canSelfUpdate && !this.coordinator.canSelfUpdate())) {
3674
3723
  await this.sendMessage(scopeId, t(locale, 'update_blocked_active'));
3675
3724
  return;
3676
3725
  }
@@ -3721,17 +3770,23 @@ export class BridgeSessionCore {
3721
3770
  this.scheduleSelfUpdateStatusPoll();
3722
3771
  return;
3723
3772
  }
3773
+ if (!this.ownsScope(status.scopeId)) {
3774
+ this.scheduleSelfUpdateStatusPoll();
3775
+ return;
3776
+ }
3724
3777
  await this.sendMessage(status.scopeId, this.formatSelfUpdateResult(status));
3725
3778
  await this.selfUpdater?.clearStatus();
3726
3779
  }
3727
3780
  formatSelfUpdateResult(status) {
3728
3781
  if (status.state === 'succeeded') {
3729
- return t(status.locale, 'update_succeeded', {
3782
+ const result = t(status.locale, 'update_succeeded', {
3730
3783
  from: status.fromVersion,
3731
3784
  to: status.toVersion ?? t(status.locale, 'unknown'),
3732
3785
  });
3786
+ return status.codexUpdate ? `${result}\n${status.codexUpdate}` : result;
3733
3787
  }
3734
- return t(status.locale, 'update_failed', { error: status.error ?? t(status.locale, 'unknown') });
3788
+ const result = t(status.locale, 'update_failed', { error: status.error ?? t(status.locale, 'unknown') });
3789
+ return status.codexUpdate ? `${result}\n${status.codexUpdate}` : result;
3735
3790
  }
3736
3791
  async handleAuthCommand(scopeId, locale, args) {
3737
3792
  const action = args[0]?.toLowerCase() ?? 'list';
@@ -3769,7 +3824,7 @@ export class BridgeSessionCore {
3769
3824
  record.messageId = messageId;
3770
3825
  }
3771
3826
  async handleAuthUseCommand(scopeId, locale, args) {
3772
- if (this.activeTurns.size > 0 || this.store.countPendingApprovals() > 0 || this.pendingUserInputs.size > 0 || this.pendingMcpElicitations.size > 0) {
3827
+ if (this.hasLocalBlockingActivity()) {
3773
3828
  await this.sendMessage(scopeId, t(locale, 'auth_reload_blocked_active'));
3774
3829
  return;
3775
3830
  }
@@ -3807,7 +3862,7 @@ export class BridgeSessionCore {
3807
3862
  await this.sendMessage(scopeId, renderAuthListMessage(locale, state, parseWeixinBridgeScope(scopeId) !== null));
3808
3863
  return;
3809
3864
  }
3810
- this.store.setCodexAuthCandidateDisabled(candidate.name, disabled);
3865
+ this.store.setCodexAuthCandidateDisabled(candidate.name, disabled, this.authRuntimeId());
3811
3866
  await this.sendMessage(scopeId, t(locale, disabled ? 'auth_candidate_disabled' : 'auth_candidate_enabled', {
3812
3867
  value: candidate.name,
3813
3868
  }));
@@ -3825,7 +3880,7 @@ export class BridgeSessionCore {
3825
3880
  return latest;
3826
3881
  }
3827
3882
  async handleAuthAddCommand(scopeId, locale, args) {
3828
- if (this.activeTurns.size > 0 || this.store.countPendingApprovals() > 0 || this.pendingUserInputs.size > 0 || this.pendingMcpElicitations.size > 0) {
3883
+ if (this.hasLocalBlockingActivity()) {
3829
3884
  await this.sendMessage(scopeId, t(locale, 'auth_reload_blocked_active'));
3830
3885
  return;
3831
3886
  }
@@ -4259,7 +4314,7 @@ export class BridgeSessionCore {
4259
4314
  return;
4260
4315
  }
4261
4316
  const disabled = !candidate.disabled;
4262
- this.store.setCodexAuthCandidateDisabled(candidate.name, disabled);
4317
+ this.store.setCodexAuthCandidateDisabled(candidate.name, disabled, this.authRuntimeId());
4263
4318
  const state = await this.listCodexAuthState();
4264
4319
  record.candidates = state.candidates;
4265
4320
  await this.messaging.answerCallback(event.callbackQueryId, t(locale, disabled ? 'auth_candidate_disabled_short' : 'auth_candidate_enabled_short'));
@@ -4277,7 +4332,7 @@ export class BridgeSessionCore {
4277
4332
  await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_choice_mismatch'));
4278
4333
  return;
4279
4334
  }
4280
- if (this.activeTurns.size > 0 || this.store.countPendingApprovals() > 0 || this.pendingUserInputs.size > 0 || this.pendingMcpElicitations.size > 0) {
4335
+ if (this.hasLocalBlockingActivity()) {
4281
4336
  await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_reload_blocked_active'));
4282
4337
  return;
4283
4338
  }
@@ -4299,7 +4354,7 @@ export class BridgeSessionCore {
4299
4354
  if (!this.pendingAuthRotation || this.authRotationInProgress) {
4300
4355
  return false;
4301
4356
  }
4302
- if (this.activeTurns.size > 0 || this.store.countPendingApprovals() > 0 || this.pendingUserInputs.size > 0 || this.pendingMcpElicitations.size > 0) {
4357
+ if (this.hasLocalBlockingActivity()) {
4303
4358
  return false;
4304
4359
  }
4305
4360
  const rotation = this.pendingAuthRotation;
@@ -4398,13 +4453,19 @@ export class BridgeSessionCore {
4398
4453
  return candidate ? { candidate, fromLabel: state.currentLabel, toLabel: await authPathDisplayLabel(candidate.path) } : null;
4399
4454
  }
4400
4455
  async listCodexAuthState() {
4401
- const state = await listCodexAuthState(this.store.listDisabledCodexAuthCandidateNames());
4456
+ const state = await listCodexAuthState(this.store.listDisabledCodexAuthCandidateNames(this.authRuntimeId()), this.resolveAuthDir());
4402
4457
  const snapshots = await this.readCodexAuthQuotaSnapshots();
4403
4458
  state.candidates.forEach((candidate) => {
4404
4459
  candidate.quota = snapshots[candidate.name] ?? null;
4405
4460
  });
4406
4461
  return state;
4407
4462
  }
4463
+ resolveAuthDir() {
4464
+ return this.config.codexAuthDir
4465
+ ?? (this.config.tgMultiBotMode ? this.config.codexHome : null)
4466
+ ?? process.env.CODEX_AUTH_DIR
4467
+ ?? path.join(os.homedir(), '.codex');
4468
+ }
4408
4469
  async readCodexAuthSwitchLabels(candidate) {
4409
4470
  const state = await this.listCodexAuthState();
4410
4471
  return {
@@ -4419,7 +4480,7 @@ export class BridgeSessionCore {
4419
4480
  };
4420
4481
  }
4421
4482
  async switchCodexAuthAndRestart(scopeId, locale, candidate, automatic) {
4422
- const result = await switchCodexAuth(candidate.path);
4483
+ const result = await switchCodexAuth(candidate.path, this.resolveAuthDir());
4423
4484
  this.authRotationFailedTargets.delete(candidate.path);
4424
4485
  this.pendingTurnErrors.clear();
4425
4486
  this.attachedThreads.clear();
@@ -4544,11 +4605,12 @@ export class BridgeSessionCore {
4544
4605
  t(locale, 'status_codex_local_tokens', {
4545
4606
  total: formatTokenCount(stats.totals.totalTokens),
4546
4607
  input: formatTokenCount(stats.totals.inputTokens),
4608
+ visible: formatTokenCount(Math.max(0, stats.totals.outputTokens - stats.totals.reasoningOutputTokens)),
4547
4609
  output: formatTokenCount(stats.totals.outputTokens),
4548
4610
  cached: formatTokenCount(stats.totals.cachedInputTokens),
4549
4611
  reasoning: formatTokenCount(stats.totals.reasoningOutputTokens),
4550
4612
  }),
4551
- ...this.formatCodexLocalOutputSpeedStatusLines(locale, stats),
4613
+ ...this.formatCodexLocalResponseThroughputStatusLines(locale, stats),
4552
4614
  t(locale, 'status_codex_local_snapshot_at', {
4553
4615
  value: formatLocalTimestamp(snapshot.computedAtMs / 1000),
4554
4616
  }),
@@ -4559,16 +4621,21 @@ export class BridgeSessionCore {
4559
4621
  return [t(locale, 'status_codex_local_usage_unavailable', { error: formatShortStatusError(error) })];
4560
4622
  }
4561
4623
  }
4562
- formatCodexLocalOutputSpeedStatusLines(locale, stats) {
4563
- const speed = stats.outputSpeed;
4564
- if (speed.samples === 0 || speed.outputTokens <= 0 || speed.seconds <= 0) {
4624
+ formatCodexLocalResponseThroughputStatusLines(locale, stats) {
4625
+ const throughput = stats.responseThroughput;
4626
+ if (throughput.completedTurns === 0
4627
+ || throughput.visibleOutputTokens <= 0
4628
+ || throughput.seconds <= 0
4629
+ || throughput.recentCompletedTurns === 0
4630
+ || throughput.recentVisibleOutputTokens <= 0
4631
+ || throughput.recentSeconds <= 0) {
4565
4632
  return [];
4566
4633
  }
4567
- const avg = speed.outputTokens / speed.seconds;
4568
- return [t(locale, 'status_codex_local_speed', {
4569
- avg: formatCompactNumber(avg),
4570
- latest: speed.latestTokensPerSecond === null ? t(locale, 'unknown') : formatCompactNumber(speed.latestTokensPerSecond),
4571
- samples: formatTokenCount(speed.samples),
4634
+ return [t(locale, 'status_codex_local_throughput', {
4635
+ overall: formatCompactNumber(throughput.visibleOutputTokens / throughput.seconds),
4636
+ recent: formatCompactNumber(throughput.recentVisibleOutputTokens / throughput.recentSeconds),
4637
+ recentTurns: formatTokenCount(throughput.recentCompletedTurns),
4638
+ turns: formatTokenCount(throughput.completedTurns),
4572
4639
  })];
4573
4640
  }
4574
4641
  async resolveFastStatusLabel(locale, settings) {
@@ -4604,14 +4671,14 @@ export class BridgeSessionCore {
4604
4671
  await this.localUsageRefresh;
4605
4672
  }
4606
4673
  async refreshCodexLocalUsageStats() {
4607
- const stats = await readCodexLocalUsageStats();
4674
+ const stats = await readCodexLocalUsageStats(this.config.codexHome ?? undefined);
4608
4675
  const snapshot = { computedAtMs: Date.now(), stats };
4609
4676
  this.localUsageCache = snapshot;
4610
4677
  this.localUsageCacheLoaded = true;
4611
4678
  await writeCodexLocalUsageSnapshot(this.codexLocalUsageSnapshotPath(), snapshot);
4612
4679
  }
4613
4680
  codexLocalUsageSnapshotPath() {
4614
- return path.join(path.dirname(this.config.statusPath), CODEX_LOCAL_USAGE_SNAPSHOT_FILENAME);
4681
+ return path.join(path.dirname(this.config.statusPath), this.runtimeSnapshotFilename(CODEX_LOCAL_USAGE_SNAPSHOT_FILENAME));
4615
4682
  }
4616
4683
  async refreshCurrentCodexAuthQuota(state) {
4617
4684
  const candidate = state.candidates.find(entry => entry.isCurrent);
@@ -4663,7 +4730,13 @@ export class BridgeSessionCore {
4663
4730
  await fs.rename(temporaryPath, snapshotPath);
4664
4731
  }
4665
4732
  codexAuthQuotaSnapshotPath() {
4666
- return path.join(path.dirname(this.config.statusPath), CODEX_AUTH_QUOTA_SNAPSHOT_FILENAME);
4733
+ return path.join(path.dirname(this.config.statusPath), this.runtimeSnapshotFilename(CODEX_AUTH_QUOTA_SNAPSHOT_FILENAME));
4734
+ }
4735
+ runtimeSnapshotFilename(filename) {
4736
+ if (!this.config.tgScopeBotId) {
4737
+ return filename;
4738
+ }
4739
+ return filename.replace(/\.json$/, `-${this.config.tgScopeBotId}.json`);
4667
4740
  }
4668
4741
  async sendThreadContextSummary(scopeId, locale, threadId) {
4669
4742
  try {
@@ -5582,6 +5655,9 @@ export class BridgeSessionCore {
5582
5655
  }
5583
5656
  async cleanupStaleTurnPreviews() {
5584
5657
  for (const preview of this.store.listActiveTurnPreviews()) {
5658
+ if (!this.ownsScope(preview.scopeId)) {
5659
+ continue;
5660
+ }
5585
5661
  if (!this.messaging.canSendToScope(preview.scopeId)) {
5586
5662
  this.store.removeActiveTurnPreview(preview.turnId);
5587
5663
  this.logger.info('telegram.preview_dropped_disabled_channel', {
@@ -7113,11 +7189,11 @@ function parseActiveTurnKey(key) {
7113
7189
  turnId: decodeURIComponent(key.slice(split + 1)),
7114
7190
  };
7115
7191
  }
7116
- function codexAuthDir() {
7117
- return process.env.CODEX_AUTH_DIR || path.join(os.homedir(), '.codex');
7192
+ function codexAuthDir(explicitAuthDir = null) {
7193
+ return explicitAuthDir || process.env.CODEX_AUTH_DIR || path.join(os.homedir(), '.codex');
7118
7194
  }
7119
- async function listCodexAuthState(disabledNames = new Set()) {
7120
- const authDir = codexAuthDir();
7195
+ async function listCodexAuthState(disabledNames = new Set(), explicitAuthDir = null) {
7196
+ const authDir = codexAuthDir(explicitAuthDir);
7121
7197
  const authPath = path.join(authDir, 'auth.json');
7122
7198
  const currentTargetPath = await resolveCurrentAuthTarget(authDir, authPath);
7123
7199
  const candidates = [];
@@ -7216,8 +7292,8 @@ async function pointCodexAuthAtTarget(authDir, authPath, targetPath) {
7216
7292
  throw error;
7217
7293
  }
7218
7294
  }
7219
- async function switchCodexAuth(targetPath) {
7220
- const state = await listCodexAuthState();
7295
+ async function switchCodexAuth(targetPath, explicitAuthDir = null) {
7296
+ const state = await listCodexAuthState(new Set(), explicitAuthDir);
7221
7297
  const candidate = state.candidates.find(entry => entry.path === targetPath);
7222
7298
  if (!candidate) {
7223
7299
  throw new Error(`Auth candidate is no longer available: ${path.basename(targetPath)}`);
@@ -7,12 +7,15 @@ export interface WeixinBridgeScope {
7
7
  accountId: string;
8
8
  fromUserId: string;
9
9
  }
10
+ export interface TelegramBridgeTarget extends TelegramScope {
11
+ botId: string | null;
12
+ }
10
13
  export declare function isBridgeScopedKey(key: string): boolean;
11
14
  /** Wrap legacy Telegram inner scope (`chat::topic`) for storage and routing. */
12
- export declare function toTelegramBridgeScopeId(telegramInnerScopeId: string): string;
15
+ export declare function toTelegramBridgeScopeId(telegramInnerScopeId: string, botId?: string | null): string;
13
16
  /** Strip `telegram:` prefix; returns `null` if not a Telegram bridge scope. */
14
17
  export declare function telegramInnerScopeFromBridge(bridgeScopeId: string): string | null;
15
- export declare function parseTelegramTargetFromBridgeScope(bridgeScopeId: string): TelegramScope;
18
+ export declare function parseTelegramTargetFromBridgeScope(bridgeScopeId: string): TelegramBridgeTarget;
16
19
  /** Parse `weixin:<accountId>:<from_user_id>`; returns `null` if not a Weixin scope. */
17
20
  export declare function parseWeixinBridgeScope(bridgeScopeId: string): WeixinBridgeScope | null;
18
21
  export declare function toWeixinBridgeScopeId(accountId: string, fromUserId: string): string;
@@ -7,8 +7,8 @@ export function isBridgeScopedKey(key) {
7
7
  return key.startsWith(BRIDGE_SCOPE_TELEGRAM_PREFIX) || key.startsWith(BRIDGE_SCOPE_WEIXIN_PREFIX);
8
8
  }
9
9
  /** Wrap legacy Telegram inner scope (`chat::topic`) for storage and routing. */
10
- export function toTelegramBridgeScopeId(telegramInnerScopeId) {
11
- return `${BRIDGE_SCOPE_TELEGRAM_PREFIX}${telegramInnerScopeId}`;
10
+ export function toTelegramBridgeScopeId(telegramInnerScopeId, botId = null) {
11
+ return `${BRIDGE_SCOPE_TELEGRAM_PREFIX}${botId ? `${botId}:` : ''}${telegramInnerScopeId}`;
12
12
  }
13
13
  /** Strip `telegram:` prefix; returns `null` if not a Telegram bridge scope. */
14
14
  export function telegramInnerScopeFromBridge(bridgeScopeId) {
@@ -22,7 +22,11 @@ export function parseTelegramTargetFromBridgeScope(bridgeScopeId) {
22
22
  if (inner === null) {
23
23
  throw new Error(`Expected ${BRIDGE_SCOPE_TELEGRAM_PREFIX} scope, got: ${bridgeScopeId}`);
24
24
  }
25
- return parseTelegramScopeId(inner);
25
+ const namespaced = /^(bot\d+):(.*)$/.exec(inner);
26
+ if (!namespaced) {
27
+ return { ...parseTelegramScopeId(inner), botId: null };
28
+ }
29
+ return { ...parseTelegramScopeId(namespaced[2]), botId: namespaced[1] };
26
30
  }
27
31
  /** Parse `weixin:<accountId>:<from_user_id>`; returns `null` if not a Weixin scope. */
28
32
  export function parseWeixinBridgeScope(bridgeScopeId) {
package/dist/i18n.d.ts CHANGED
@@ -111,8 +111,8 @@ declare const MESSAGES: {
111
111
  readonly status_codex_usage_reset: ", resets {value}";
112
112
  readonly status_codex_usage_unavailable: "Codex usage: unavailable ({error})";
113
113
  readonly status_codex_local_history: "Codex local history: {sessions} sessions, {turns} turns, {events} usage records";
114
- readonly status_codex_local_tokens: "Codex local tokens: total {total}; input {input}, output {output}, cached input {cached}, reasoning output {reasoning}";
115
- readonly status_codex_local_speed: "Codex local output speed: avg {avg} token/s, latest {latest} token/s ({samples} samples)";
114
+ readonly status_codex_local_tokens: "Codex local tokens: total {total}; input {input}, visible output {visible}, reasoning output {reasoning}, total output {output}, cached input {cached}";
115
+ readonly status_codex_local_throughput: "Codex visible reply throughput (end-to-end, excluding reasoning): overall {overall} token/s, last {recentTurns} completed turns {recent} token/s ({turns} completed turns sampled)";
116
116
  readonly status_codex_local_snapshot_at: "Codex local stats snapshot: {value}";
117
117
  readonly status_codex_local_usage_refreshing: "Codex local history: building snapshot in background";
118
118
  readonly status_codex_local_usage_unavailable: "Codex local history: unavailable ({error})";
@@ -675,8 +675,8 @@ declare const MESSAGES: {
675
675
  readonly status_codex_usage_reset: ",重置时间 {value}";
676
676
  readonly status_codex_usage_unavailable: "Codex 用量:无法获取({error})";
677
677
  readonly status_codex_local_history: "Codex 本地历史:{sessions} 个会话,{turns} 轮,{events} 条用量记录";
678
- readonly status_codex_local_tokens: "Codex 本地 Token:总计 {total};输入 {input},输出 {output},缓存输入 {cached},推理输出 {reasoning}";
679
- readonly status_codex_local_speed: "Codex 本地输出速度:平均 {avg} token/s,最近 {latest} token/s({samples} 个样本)";
678
+ readonly status_codex_local_tokens: "Codex 本地 Token:总计 {total};输入 {input},可见输出 {visible},推理输出 {reasoning},总输出 {output},缓存输入 {cached}";
679
+ readonly status_codex_local_throughput: "Codex 可见答复吞吐(端到端,排除推理 token):整体 {overall} token/s,最近 {recentTurns} 个完成轮次 {recent} token/s({turns} 个完成轮次样本)";
680
680
  readonly status_codex_local_snapshot_at: "Codex 本地统计快照:{value}";
681
681
  readonly status_codex_local_usage_refreshing: "Codex 本地历史:正在后台生成统计快照";
682
682
  readonly status_codex_local_usage_unavailable: "Codex 本地历史:无法获取({error})";
package/dist/i18n.js CHANGED
@@ -109,8 +109,8 @@ const MESSAGES = {
109
109
  status_codex_usage_reset: ', resets {value}',
110
110
  status_codex_usage_unavailable: 'Codex usage: unavailable ({error})',
111
111
  status_codex_local_history: 'Codex local history: {sessions} sessions, {turns} turns, {events} usage records',
112
- status_codex_local_tokens: 'Codex local tokens: total {total}; input {input}, output {output}, cached input {cached}, reasoning output {reasoning}',
113
- status_codex_local_speed: 'Codex local output speed: avg {avg} token/s, latest {latest} token/s ({samples} samples)',
112
+ status_codex_local_tokens: 'Codex local tokens: total {total}; input {input}, visible output {visible}, reasoning output {reasoning}, total output {output}, cached input {cached}',
113
+ status_codex_local_throughput: 'Codex visible reply throughput (end-to-end, excluding reasoning): overall {overall} token/s, last {recentTurns} completed turns {recent} token/s ({turns} completed turns sampled)',
114
114
  status_codex_local_snapshot_at: 'Codex local stats snapshot: {value}',
115
115
  status_codex_local_usage_refreshing: 'Codex local history: building snapshot in background',
116
116
  status_codex_local_usage_unavailable: 'Codex local history: unavailable ({error})',
@@ -673,8 +673,8 @@ const MESSAGES = {
673
673
  status_codex_usage_reset: ',重置时间 {value}',
674
674
  status_codex_usage_unavailable: 'Codex 用量:无法获取({error})',
675
675
  status_codex_local_history: 'Codex 本地历史:{sessions} 个会话,{turns} 轮,{events} 条用量记录',
676
- status_codex_local_tokens: 'Codex 本地 Token:总计 {total};输入 {input},输出 {output},缓存输入 {cached},推理输出 {reasoning}',
677
- status_codex_local_speed: 'Codex 本地输出速度:平均 {avg} token/s,最近 {latest} token/s({samples} 个样本)',
676
+ status_codex_local_tokens: 'Codex 本地 Token:总计 {total};输入 {input},可见输出 {visible},推理输出 {reasoning},总输出 {output},缓存输入 {cached}',
677
+ status_codex_local_throughput: 'Codex 可见答复吞吐(端到端,排除推理 token):整体 {overall} token/s,最近 {recentTurns} 个完成轮次 {recent} token/s({turns} 个完成轮次样本)',
678
678
  status_codex_local_snapshot_at: 'Codex 本地统计快照:{value}',
679
679
  status_codex_local_usage_refreshing: 'Codex 本地历史:正在后台生成统计快照',
680
680
  status_codex_local_usage_unavailable: 'Codex 本地历史:无法获取({error})',