@foxden-app/foxclaw 0.7.3 → 0.11.0

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 (63) hide show
  1. package/.env.example +22 -8
  2. package/CHANGELOG.md +134 -0
  3. package/README.md +21 -1
  4. package/README_EN.md +21 -1
  5. package/dist/antigravity/adapter.d.ts +18 -0
  6. package/dist/antigravity/adapter.js +394 -0
  7. package/dist/antigravity/auth.d.ts +126 -0
  8. package/dist/antigravity/auth.js +933 -0
  9. package/dist/antigravity/client.d.ts +32 -0
  10. package/dist/antigravity/client.js +244 -0
  11. package/dist/antigravity/controller.d.ts +101 -0
  12. package/dist/antigravity/controller.js +2007 -0
  13. package/dist/antigravity/conversations.d.ts +29 -0
  14. package/dist/antigravity/conversations.js +289 -0
  15. package/dist/antigravity/events.d.ts +102 -0
  16. package/dist/antigravity/events.js +143 -0
  17. package/dist/antigravity/runtime.d.ts +49 -0
  18. package/dist/antigravity/runtime.js +82 -0
  19. package/dist/antigravity/subagents.d.ts +66 -0
  20. package/dist/antigravity/subagents.js +459 -0
  21. package/dist/channels/telegram/telegram_channel_adapter.d.ts +10 -3
  22. package/dist/channels/telegram/telegram_channel_adapter.js +2 -1
  23. package/dist/channels/weixin/weixin_channel_adapter.d.ts +5 -1
  24. package/dist/codex_app/adapter.d.ts +20 -0
  25. package/dist/codex_app/adapter.js +213 -0
  26. package/dist/codex_app/client.d.ts +1 -1
  27. package/dist/codex_app/client.js +12 -0
  28. package/dist/config.d.ts +14 -0
  29. package/dist/config.js +65 -16
  30. package/dist/controller/controller.d.ts +12 -5
  31. package/dist/controller/controller.js +109 -8
  32. package/dist/core/attachments.d.ts +11 -0
  33. package/dist/core/attachments.js +56 -0
  34. package/dist/core/engine_spi.d.ts +77 -0
  35. package/dist/core/engine_spi.js +1 -0
  36. package/dist/core/orchestrator.d.ts +106 -0
  37. package/dist/core/orchestrator.js +1556 -0
  38. package/dist/core/stream_preview.d.ts +29 -0
  39. package/dist/core/stream_preview.js +146 -0
  40. package/dist/core/turn_queue.d.ts +19 -0
  41. package/dist/core/turn_queue.js +41 -0
  42. package/dist/i18n.d.ts +6 -0
  43. package/dist/i18n.js +49 -0
  44. package/dist/main.js +375 -446
  45. package/dist/opencode/adapter.d.ts +11 -0
  46. package/dist/opencode/adapter.js +169 -0
  47. package/dist/opencode/controller.js +2 -2
  48. package/dist/store/database.d.ts +44 -0
  49. package/dist/store/database.js +275 -43
  50. package/dist/store/token_usage.d.ts +23 -0
  51. package/dist/store/token_usage.js +61 -0
  52. package/dist/telegram/api.d.ts +1 -0
  53. package/dist/telegram/api.js +80 -3
  54. package/dist/telegram/bot_home.js +5 -4
  55. package/dist/telegram/gateway.d.ts +4 -0
  56. package/dist/telegram/gateway.js +11 -0
  57. package/dist/types.d.ts +3 -0
  58. package/dist/update.d.ts +6 -0
  59. package/dist/update.js +95 -7
  60. package/dist/voice/tts.js +4 -2
  61. package/docs/devlogs//346/212/200/346/234/257/351/200/232/350/256/257/02./347/273/237/344/270/200/351/200/232/351/201/223/350/260/203/345/272/246/345/231/250/344/270/216/345/217/257/346/217/222/346/213/224/345/274/225/346/223/216SPI/357/274/232FoxClaw/344/273/216/345/215/225/344/275/223/350/265/260/345/220/221/345/244/232Agent/345/272/225/345/272/247.md +192 -0
  62. package/docs/devlogs//347/254/254/344/270/200/344/272/272/347/247/260/347/211/210/02./347/257/207/344/272/214_/345/275/223/344/270/200/344/270/252/346/241/245/346/216/245/345/231/250/345/274/200/345/247/213/345/255/246/344/274/232/345/220/254/346/207/202/346/211/200/346/234/211Agent.md +45 -0
  63. package/package.json +2 -2
package/dist/main.js CHANGED
@@ -7,8 +7,9 @@ import process from 'node:process';
7
7
  import { createInterface } from 'node:readline/promises';
8
8
  import { spawnSync } from 'node:child_process';
9
9
  import { fileURLToPath } from 'node:url';
10
- import { APP_HOME, buildCodexApiProviderOverrides, DEFAULT_CODEX_TELEGRAM_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, getLoadedEnvPath, loadConfig, loadEnv, } from './config.js';
10
+ import { APP_HOME, buildCodexApiProviderOverrides, DEFAULT_CODEX_TELEGRAM_HOME, DEFAULT_ANTIGRAVITY_TELEGRAM_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, getLoadedEnvPath, loadConfig, loadEnv, } from './config.js';
11
11
  import { createAuthRefreshNotificationAggregator, } from './auth/notifications.js';
12
+ import { getAntigravityTelegramCommands, getTelegramCommands } from './i18n.js';
12
13
  import { installBundledCodexSkills } from './codex_skills.js';
13
14
  import { acquireProcessLock, LockHeldError } from './lock.js';
14
15
  import { readRuntimeStatus, writeRuntimeStatus } from './runtime.js';
@@ -190,6 +191,9 @@ async function main() {
190
191
  ...(process.env.CODEX_CLI_BIN || resolveCommand('codex')
191
192
  ? { codexCliBin: process.env.CODEX_CLI_BIN || resolveCommand('codex') }
192
193
  : {}),
194
+ ...(process.env.AGY_CLI_BIN || resolveCommand('agy')
195
+ ? { agyCliBin: process.env.AGY_CLI_BIN || resolveCommand('agy') }
196
+ : {}),
193
197
  ...(notificationFile ? { notificationFile } : {}),
194
198
  };
195
199
  const outcome = performSelfUpdate(options);
@@ -345,7 +349,8 @@ async function runSendVoiceCli() {
345
349
  throw new Error('Telegram voice files must be 50MB or smaller.');
346
350
  }
347
351
  const inferredBotId = parsed.botId ?? inferTelegramBotId(process.env.CODEX_HOME) ?? inferTelegramBotId(config.codexHome);
348
- const { botId, botToken } = resolveTelegramVoiceTarget(config.tgBotTokens, inferredBotId);
352
+ const allTokens = [...config.codexBotTokens, ...config.antigravityBotTokens];
353
+ const { botId, botToken } = resolveTelegramVoiceTarget(allTokens, inferredBotId);
349
354
  const { BridgeStore } = await import('./store/database.js');
350
355
  const store = new BridgeStore(config.storePath);
351
356
  let chatId = parsed.chatId;
@@ -390,7 +395,8 @@ async function runSendMediaCli() {
390
395
  }
391
396
  const plan = planTelegramOutboundMedia(filePath);
392
397
  const inferredBotId = parsed.botId ?? inferTelegramBotId(process.env.CODEX_HOME) ?? inferTelegramBotId(config.codexHome);
393
- const { botId, botToken } = resolveTelegramVoiceTarget(config.tgBotTokens, inferredBotId);
398
+ const allTokens = [...config.codexBotTokens, ...config.antigravityBotTokens];
399
+ const { botId, botToken } = resolveTelegramVoiceTarget(allTokens, inferredBotId);
394
400
  const { BridgeStore } = await import('./store/database.js');
395
401
  const store = new BridgeStore(config.storePath);
396
402
  let chatId = parsed.chatId;
@@ -522,6 +528,17 @@ function formatRuntimeStatusSummary(status) {
522
528
  else if (status.codexHome) {
523
529
  lines.push(`Codex home: ${status.codexHome}`);
524
530
  }
531
+ const agyHomes = (status.bots ?? [])
532
+ .flatMap(bot => bot.antigravityHome ? [{ label: bot.username ? `@${bot.username}` : bot.id, home: bot.antigravityHome }] : []);
533
+ if (agyHomes.length > 1) {
534
+ lines.push('Antigravity homes:');
535
+ for (const { label, home } of agyHomes) {
536
+ lines.push(` ${label}: ${home}`);
537
+ }
538
+ }
539
+ else if (agyHomes.length === 1) {
540
+ lines.push(`Antigravity home: ${agyHomes[0].home}`);
541
+ }
525
542
  if (status.codexAppServer) {
526
543
  lines.push(`App server: ${status.codexAppServer.running ? 'running' : 'stopped'}${status.codexAppServer.pid ? ` pid=${status.codexAppServer.pid}` : ''}${status.codexAppServer.port ? ` port=${status.codexAppServer.port}` : ''}`);
527
544
  }
@@ -588,7 +605,7 @@ function truncateStatusLine(value, maxLength) {
588
605
  return normalized.length <= maxLength ? normalized : `${normalized.slice(0, Math.max(0, maxLength - 1))}…`;
589
606
  }
590
607
  async function runServeCli() {
591
- const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror }, { CrossNodeAuthSync }, { OpencodeTelegramRuntime },] = await Promise.all([
608
+ const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror }, { CrossNodeAuthSync }, { OpencodeTelegramRuntime }, { UnifiedBridgeCore }, { AntigravityAppClient }, { AntigravityAuthManager, linkAntigravityAuthTokens },] = await Promise.all([
592
609
  import('./channels/bridge_messaging_router.js'),
593
610
  import('./channels/telegram/telegram_messaging_port.js'),
594
611
  import('./channels/weixin/weixin_channel_adapter.js'),
@@ -604,6 +621,9 @@ async function runServeCli() {
604
621
  import('./auth/mirror.js'),
605
622
  import('./auth/cross_node_sync.js'),
606
623
  import('./opencode/runtime.js'),
624
+ import('./antigravity/controller.js'),
625
+ import('./antigravity/client.js'),
626
+ import('./antigravity/auth.js'),
607
627
  ]);
608
628
  const config = loadConfig();
609
629
  const logger = new Logger(config.logLevel, config.logPath);
@@ -619,386 +639,250 @@ async function runServeCli() {
619
639
  let activeAuthMirror = null;
620
640
  let activeAuthSync = null;
621
641
  let activeOpencodeRuntime = null;
642
+ let sharedCodexApp = null;
643
+ let sharedAntigravityApp = null;
644
+ let sharedAntigravityAuth = null;
622
645
  try {
623
646
  store = new BridgeStore(config.storePath);
647
+ const selfUpdater = createSelfUpdateRuntime({
648
+ entryPoint,
649
+ nodePath: process.execPath,
650
+ version: readPackageVersion(),
651
+ statusPath: config.statusPath,
652
+ logPath: path.join(APP_HOME, 'logs', 'update.log'),
653
+ codexCliBin: config.codexCliBin,
654
+ agyCliBin: config.antigravityCliBin,
655
+ });
656
+ sharedAntigravityAuth = new AntigravityAuthManager(config.antigravityAuthDir, logger);
657
+ sharedAntigravityApp = new AntigravityAppClient(config.antigravityCliBin, logger);
658
+ if (config.codexCliBin) {
659
+ sharedCodexApp = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger, config.codexHome ? { CODEX_HOME: config.codexHome } : null, codexApiProviderOverrides);
660
+ }
624
661
  if (config.opencodeBotToken) {
625
662
  activeOpencodeRuntime = new OpencodeTelegramRuntime(config, store, logger);
626
663
  await activeOpencodeRuntime.start();
627
664
  logger.info('opencode.bridge.started', activeOpencodeRuntime.getRuntimeStatus());
628
665
  }
629
- if (config.tgMultiBotMode) {
630
- const seeds = [];
631
- const canonicalAuthDir = config.codexAuthDir ?? config.codexHome ?? path.join(os.homedir(), '.codex');
632
- const identities = await Promise.all(config.tgBotTokens.map(async (token) => {
633
- const bot = new TelegramGateway(token, config.tgAllowedUserId, config.tgAllowedChatId, config.telegramPollIntervalMs, store, logger, true);
634
- const id = await bot.initializeIdentity();
635
- const username = await bot.resolveUsername();
636
- return { token, bot, id, username };
637
- }));
638
- for (const { token, bot, id, username } of identities) {
639
- if (seeds.some((runtime) => runtime.id === id)) {
640
- throw new Error(`TG_BOT_TOKENS contains duplicate Telegram bot identity: ${id}`);
641
- }
642
- const sharedDefaultRuntime = config.tgDefaultRuntimeBotToken === token;
643
- const home = prepareTelegramBotHome(DEFAULT_CODEX_TELEGRAM_HOME, id, username, sharedDefaultRuntime ? (config.codexHome ?? path.join(os.homedir(), '.codex')) : null);
644
- const authDir = sharedDefaultRuntime ? canonicalAuthDir : home;
645
- if (!sharedDefaultRuntime) {
646
- fs.mkdirSync(home, { recursive: true, mode: 0o700 });
647
- }
648
- installBundledCodexSkills(packageRoot, home);
649
- const runtimeConfig = {
650
- ...config,
651
- tgBotToken: token,
652
- tgBotTokens: [token],
653
- tgScopeBotId: id,
654
- codexAuthDir: sharedDefaultRuntime ? config.codexAuthDir : home,
655
- codexHome: home,
656
- codexAppAutolaunch: sharedDefaultRuntime ? config.codexAppAutolaunch : false,
657
- codexAppServerStatePath: sharedDefaultRuntime
658
- ? config.codexAppServerStatePath
659
- : path.join(APP_HOME, 'runtime', `codex-app-server-${id}.json`),
660
- codexAppServerLogPath: sharedDefaultRuntime
661
- ? config.codexAppServerLogPath
662
- : path.join(APP_HOME, 'logs', `codex-app-server-${id}.log`),
663
- };
664
- const childEnv = { CODEX_HOME: home };
665
- const app = new CodexAppClient(runtimeConfig.codexCliBin, runtimeConfig.codexAppLaunchCmd, runtimeConfig.codexAppAutolaunch, runtimeConfig.codexAppServerStatePath, runtimeConfig.codexAppServerLogPath, logger, childEnv, [
666
+ const totalBots = config.codexBotTokens.length + config.antigravityBotTokens.length;
667
+ const namespacedScopes = totalBots > 1;
668
+ const seeds = [];
669
+ const canonicalAuthDir = config.codexAuthDir ?? config.codexHome ?? path.join(os.homedir(), '.codex');
670
+ const specs = [
671
+ ...config.codexBotTokens.map((token, i) => ({
672
+ token,
673
+ backend: 'codex',
674
+ isDefaultRuntimeForBackend: config.tgDefaultRuntimeBotToken
675
+ ? config.tgDefaultRuntimeBotToken === token
676
+ : i === 0,
677
+ })),
678
+ ...config.antigravityBotTokens.map((token, i) => ({
679
+ token,
680
+ backend: 'antigravity',
681
+ isDefaultRuntimeForBackend: config.antigravityDefaultRuntimeBotToken
682
+ ? config.antigravityDefaultRuntimeBotToken === token
683
+ : i === 0,
684
+ })),
685
+ ];
686
+ const identities = await Promise.all(specs.map(async (spec) => {
687
+ const bot = new TelegramGateway(spec.token, config.tgAllowedUserId, config.tgAllowedChatId, config.telegramPollIntervalMs, store, logger, namespacedScopes, spec.backend === 'antigravity' ? getAntigravityTelegramCommands : getTelegramCommands);
688
+ const id = await bot.initializeIdentity();
689
+ const username = await bot.resolveUsername();
690
+ return { ...spec, bot, id, username };
691
+ }));
692
+ for (const { token, backend, isDefaultRuntimeForBackend, bot, id, username } of identities) {
693
+ if (seeds.some((runtime) => runtime.id === id)) {
694
+ throw new Error(`Duplicate Telegram bot identity detected: ${id}`);
695
+ }
696
+ // Codex environment
697
+ const isCodexDefault = backend === 'codex' && isDefaultRuntimeForBackend;
698
+ const codexHome = isCodexDefault
699
+ ? (config.codexHome ?? path.join(os.homedir(), '.codex'))
700
+ : prepareTelegramBotHome(DEFAULT_CODEX_TELEGRAM_HOME, id, username, null);
701
+ const codexAuthDir = isCodexDefault ? (config.codexAuthDir ?? codexHome) : codexHome;
702
+ if (!isCodexDefault) {
703
+ fs.mkdirSync(codexHome, { recursive: true, mode: 0o700 });
704
+ }
705
+ installBundledCodexSkills(packageRoot, codexHome);
706
+ const codexAppAutolaunch = isCodexDefault ? config.codexAppAutolaunch : false;
707
+ const codexAppServerStatePath = isCodexDefault
708
+ ? config.codexAppServerStatePath
709
+ : path.join(APP_HOME, 'runtime', `codex-app-server-${id}.json`);
710
+ const codexAppServerLogPath = isCodexDefault
711
+ ? config.codexAppServerLogPath
712
+ : path.join(APP_HOME, 'logs', `codex-app-server-${id}.log`);
713
+ const codexApp = (isCodexDefault && sharedCodexApp)
714
+ ? sharedCodexApp
715
+ : new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, codexAppAutolaunch, codexAppServerStatePath, codexAppServerLogPath, logger, { CODEX_HOME: codexHome }, [
666
716
  ...codexApiProviderOverrides,
667
- ...(sharedDefaultRuntime ? [] : ['cli_auth_credentials_store="file"']),
717
+ ...(isCodexDefault ? [] : ['cli_auth_credentials_store="file"']),
668
718
  ]);
669
- seeds.push({ id, home, authDir, sharedDefaultRuntime, config: runtimeConfig, bot, app });
670
- }
671
- let authSync = null;
672
- const authRuntimeIds = seeds.map((runtime) => runtime.id);
673
- const mirror = new AuthCandidateMirror(canonicalAuthDir, seeds.map((runtime) => ({
674
- id: runtime.id,
675
- label: runtime.bot.username ? `@${runtime.bot.username}` : runtime.id,
676
- authDir: runtime.authDir,
677
- validate: async (context) => validateRefreshedAuthCandidate(runtime, context.candidateName),
678
- notify: createAuthMirrorNotifier(store, runtime.id, runtime.bot, authNotificationAggregator, {
679
- quietAuthPoolMode: () => config.authAutoDeleteNeedsRepair,
680
- suppressBackgroundNotifications: () => true,
681
- }),
682
- })), logger, path.join(APP_HOME, 'runtime', 'auth-mirror.json'), {
683
- onSynced: async (event) => {
684
- await authSync?.publishCandidate(event.record.candidateName);
685
- },
686
- onRemoteImported: (event) => {
687
- restoreImportedAuthCandidateState(store, event.record.candidateName, authRuntimeIds);
688
- },
689
- });
690
- await mirror.initialize();
691
- activeAuthMirror = mirror;
692
- managedApps = seeds.map((runtime) => runtime.app);
693
- const selfUpdater = createSelfUpdateRuntime({
694
- entryPoint,
695
- nodePath: process.execPath,
696
- version: readPackageVersion(),
697
- statusPath: config.statusPath,
698
- logPath: path.join(APP_HOME, 'logs', 'update.log'),
699
- codexCliBin: config.codexCliBin,
700
- });
701
- const lastSelfUpdatePath = path.join(APP_HOME, 'runtime', 'last-self-update.json');
702
- let lastSelfUpdate = readSelfUpdateStatus(lastSelfUpdatePath);
703
- const runtimes = [];
704
- const localAuthRefreshLease = createLocalAuthRefreshLease();
705
- const writeAggregateStatus = (running = true) => {
706
- const statuses = runtimes.map((runtime) => runtime.core.getRuntimeStatus());
707
- const weixinStatus = activeWeixinCore?.getRuntimeStatus() ?? null;
708
- const authProactiveRefresh = newestAuthProactiveRefreshStatus([...statuses, ...(weixinStatus ? [weixinStatus] : [])]);
709
- const first = statuses[0] ?? null;
710
- writeRuntimeStatus(config.statusPath, {
711
- running,
712
- connected: running
713
- && statuses.every((status) => status.connected)
714
- && (!weixinStatus || weixinStatus.connected),
715
- userAgent: first?.userAgent ?? null,
716
- codexHome: first?.codexHome ?? null,
717
- ...(first?.codexAppServer ? { codexAppServer: first.codexAppServer } : {}),
718
- botUsername: first?.botUsername ?? null,
719
- currentBindings: store.countBindings(),
720
- pendingApprovals: store.countPendingApprovals(),
721
- pendingUserInputs: store.countPendingUserInputs(),
722
- queuedTurns: store.countQueuedTurnInputs(),
723
- activeTurns: statuses.reduce((sum, status) => sum + status.activeTurns, 0)
724
- + (weixinStatus?.activeTurns ?? 0),
725
- lastError: statuses.find((status) => status.lastError)?.lastError
726
- ?? weixinStatus?.lastError
727
- ?? null,
728
- updatedAt: new Date().toISOString(),
729
- channels: { telegram: running, weixin: running && config.wxEnabled },
730
- bots: runtimes.map((runtime, index) => ({
731
- id: runtime.id,
732
- username: statuses[index]?.botUsername ?? runtime.bot.username,
733
- connected: running && Boolean(statuses[index]?.connected),
734
- activeTurns: running ? (statuses[index]?.activeTurns ?? 0) : 0,
735
- runtimeKind: runtime.sharedDefaultRuntime ? 'default' : 'isolated',
736
- codexHome: statuses[index]?.codexHome ?? runtime.home,
737
- ...(statuses[index]?.codexAppServer ? { codexAppServer: statuses[index].codexAppServer } : {}),
738
- })),
739
- ...(weixinStatus ? {
740
- weixinRuntime: {
741
- connected: running && weixinStatus.connected,
742
- activeTurns: running ? weixinStatus.activeTurns : 0,
743
- ...(weixinStatus.codexAppServer ? { codexAppServer: weixinStatus.codexAppServer } : {}),
744
- },
745
- } : {}),
746
- authMirror: mirror.getStatus(),
747
- authSync: authSync?.getStatus() ?? null,
748
- authProactiveRefresh,
749
- lastUpdate: lastSelfUpdate,
750
- });
751
- };
752
- const authSyncLocalIdle = () => runtimes.every((runtime) => runtime.core.isIdleForServiceUpdate())
753
- && (!activeWeixinCore || activeWeixinCore.isIdleForServiceUpdate())
754
- && mirror.isIdle();
755
- const clusterUpdateScheduler = createClusterUpdateScheduler({
756
- selfUpdater,
757
- canUpdate: () => authSyncLocalIdle()
758
- && (authSync ? authSync.isIdle() : localAuthRefreshLease.isIdle()),
759
- currentVersion: readPackageVersion,
760
- logger,
761
- });
762
- const coordinator = {
763
- canSelfUpdate: () => authSyncLocalIdle()
764
- && (authSync ? authSync.isIdle() : localAuthRefreshLease.isIdle()),
765
- authCandidateUpdated: (runtimeId, candidateName) => mirror.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined),
766
- authCandidateDeleted: async (_runtimeId, candidateName, reason = null) => {
767
- await mirror.deleteCandidate(candidateName);
768
- await authSync?.publishCandidateDeletion(candidateName, reason);
769
- },
770
- recoverAuthCandidate: async (runtimeId, candidateName, options = {}) => {
771
- const local = await mirror.recoverRuntimeCandidate(runtimeId, candidateName);
772
- if (local)
773
- return true;
774
- if (options.crossNode === false)
775
- return false;
776
- const current = await mirror.readRuntimeCandidate(runtimeId, candidateName)
777
- ?? await mirror.readNewestCandidate(candidateName);
778
- return await authSync?.requestRecovery(candidateName, {
779
- accountId: current?.accountId ?? null,
780
- quotaIdentityId: current?.quotaIdentityId ?? null,
781
- lastRefreshMs: current?.lastRefreshMs ?? null,
782
- }) ?? false;
783
- },
784
- acquireAuthRefreshLease: (reason) => authSync?.acquireRefreshLease(reason)
785
- ?? localAuthRefreshLease.acquire(reason),
786
- releaseAuthRefreshLease: (leaseId) => authSync?.releaseRefreshLease(leaseId)
787
- ?? localAuthRefreshLease.release(leaseId),
788
- getAuthSyncStatus: () => authSync?.getStatus() ?? null,
789
- authSyncSafeAll: async () => {
790
- const local = await mirror.syncAllRuntimeCandidates();
791
- const remote = await authSync?.pushAll() ?? { sent: 0, skipped: 0 };
792
- return {
793
- localSynced: local.synced,
794
- localSkipped: local.skipped,
795
- sent: remote.sent,
796
- skipped: remote.skipped,
797
- };
798
- },
799
- authSyncPushAll: () => authSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
800
- authSyncTest: () => authSync?.testPeers() ?? Promise.resolve({ sent: 0, replied: 0, missing: [] }),
801
- authSyncAudit: () => authSync?.auditCluster() ?? Promise.resolve(null),
802
- statusUpdated: () => writeAggregateStatus(),
803
- getServiceStatus: async () => ({
804
- currentVersion: readPackageVersion(),
805
- bots: await Promise.all(runtimes.map(async (runtime) => {
806
- const status = runtime.core.getRuntimeStatus();
807
- return {
808
- id: runtime.id,
809
- username: status.botUsername ?? runtime.bot.username,
810
- connected: status.connected,
811
- activeTurns: status.activeTurns,
812
- runtimeKind: runtime.sharedDefaultRuntime ? 'default' : 'isolated',
813
- currentAuth: await runtime.core.getCurrentAuthLabel().catch(() => null),
814
- codexHome: status.codexHome ?? runtime.home,
815
- ...(status.codexAppServer ? { codexAppServer: status.codexAppServer } : {}),
816
- };
817
- })),
818
- ...(activeWeixinCore ? {
819
- weixinRuntime: {
820
- connected: activeWeixinCore.getRuntimeStatus().connected,
821
- activeTurns: activeWeixinCore.getRuntimeStatus().activeTurns,
822
- codexAppServer: activeWeixinCore.getRuntimeStatus().codexAppServer,
823
- },
824
- } : {}),
825
- authMirror: mirror.getStatus(),
826
- authSync: authSync?.getStatus() ?? null,
827
- authProactiveRefresh: newestAuthProactiveRefreshStatus([
828
- ...runtimes.map(runtime => runtime.core.getRuntimeStatus()),
829
- ...(activeWeixinCore ? [activeWeixinCore.getRuntimeStatus()] : []),
830
- ]),
831
- lastUpdate: lastSelfUpdate,
832
- }),
833
- selfUpdateCompleted: (status) => {
834
- lastSelfUpdate = status;
835
- writeSelfUpdateStatus(lastSelfUpdatePath, status);
836
- writeAggregateStatus();
837
- },
838
- };
839
- for (const seed of seeds) {
840
- const telegramMessaging = new TelegramMessagingPort(seed.bot);
841
- const outbound = new BridgeMessagingRouter(telegramMessaging, null);
842
- const core = new BridgeSessionCore(seed.config, store, logger, seed.bot, seed.app, outbound, selfUpdater, coordinator);
843
- runtimes.push({ ...seed, core, telegram: new TelegramChannelAdapter(core) });
719
+ // Antigravity environment
720
+ const isAntigravityDefault = backend === 'antigravity' && isDefaultRuntimeForBackend;
721
+ let antigravityHome;
722
+ let antigravityAuthDir;
723
+ let antigravityApp;
724
+ let antigravityAuth;
725
+ if (isAntigravityDefault) {
726
+ antigravityHome = os.homedir();
727
+ antigravityAuthDir = config.antigravityAuthDir;
728
+ antigravityApp = sharedAntigravityApp;
729
+ antigravityAuth = sharedAntigravityAuth;
844
730
  }
845
- if (config.wxEnabled) {
846
- const weixinApp = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger, null, codexApiProviderOverrides);
847
- const outbound = new BridgeMessagingRouter(new TelegramMessagingPort(seeds[0].bot), new WeixinMessagingPort(store, (id) => loadWeixinAccount(config.weixinAccountsDir, id)));
848
- activeWeixinCore = new BridgeSessionCore(config, store, logger, seeds[0].bot, weixinApp, outbound, selfUpdater, coordinator, false);
849
- managedApps.push(weixinApp);
850
- weixinAdapter = new WeixinChannelAdapter(activeWeixinCore, store, config, logger);
731
+ else {
732
+ antigravityHome = prepareTelegramBotHome(DEFAULT_ANTIGRAVITY_TELEGRAM_HOME, id, username, null);
733
+ antigravityAuthDir = path.join(antigravityHome, '.gemini', 'antigravity-cli');
734
+ fs.mkdirSync(antigravityAuthDir, { recursive: true, mode: 0o700 });
735
+ linkAntigravityAuthTokens(config.antigravityAuthDir, antigravityAuthDir);
736
+ antigravityApp = new AntigravityAppClient(config.antigravityCliBin, logger, { HOME: antigravityHome });
737
+ antigravityAuth = new AntigravityAuthManager(antigravityAuthDir, logger);
851
738
  }
852
- activeTelegramAdapters = runtimes.map((runtime) => runtime.telegram);
853
- if (config.authSyncEnabled) {
854
- const authSyncTransportBot = seeds[0];
855
- const authSyncTransportLabel = authSyncTransportBot.bot.username
856
- ? `@${authSyncTransportBot.bot.username}`
857
- : authSyncTransportBot.id;
858
- authSync = new CrossNodeAuthSync(buildAuthSyncConfig(config, authSyncTransportLabel), logger, {
859
- send: async (peer, envelope) => {
860
- await authSyncTransportBot.bot.sendDocument(peer, `foxclaw-auth-sync-${Date.now()}.json`, Buffer.from(envelope, 'utf8'), 'FOXCLAW_AUTH_SYNC_V1');
861
- },
862
- }, {
863
- readLocalCandidate: (candidateName) => mirror.readNewestCandidate(candidateName),
864
- listLocalCandidates: () => mirror.listNewestCandidates(),
865
- listLocalCandidateCopies: () => mirror.listCandidateCopies(),
866
- validateCandidate: async (candidateName, raw, expectedAccountId) => {
867
- if (!authSyncLocalIdle()) {
868
- return { ok: false, reason: 'runtime is not idle' };
869
- }
870
- const runtime = runtimes.find((entry) => entry.core.isIdleForServiceUpdate()) ?? runtimes[0] ?? null;
871
- if (!runtime) {
872
- return { ok: false, reason: 'no validation runtime is available' };
873
- }
874
- return runtime.core.validateExternalCodexAuthCandidate(candidateName, raw, expectedAccountId);
875
- },
876
- importCandidate: (candidateName, raw, source) => mirror.importExternalCandidate(candidateName, raw, source),
877
- markCandidateState: (candidateName, state, expected) => markAuditedAuthCandidateState(store, mirror, candidateName, state, expected, authRuntimeIds),
878
- deleteLocalCandidate: async (candidateName, source) => {
879
- if (!authSyncLocalIdle()) {
880
- return { ok: false, deleted: false, reason: 'runtime is not idle' };
881
- }
882
- await mirror.deleteCandidate(candidateName);
883
- store.deleteCodexAuthCandidate(candidateName);
884
- await Promise.all(runtimes.map(runtime => runtime.core.handleExternalCodexAuthCandidateDeleted(candidateName, source.reason ?? null)));
885
- if (activeWeixinCore) {
886
- await activeWeixinCore.handleExternalCodexAuthCandidateDeleted(candidateName, source.reason ?? null);
887
- }
888
- return { ok: true, deleted: true, reason: source.reason ?? null };
889
- },
890
- scheduleServiceUpdate: (source) => clusterUpdateScheduler.schedule(source),
891
- isIdle: authSyncLocalIdle,
892
- notify: createAuthSyncNotifier(store, authSyncTransportBot.bot, authNotificationAggregator, {
893
- quietAuthPoolMode: () => config.authAutoDeleteNeedsRepair,
894
- suppressBackgroundNotifications: () => true,
895
- }),
896
- });
897
- await authSync.initialize();
898
- activeAuthSync = authSync;
899
- attachTelegramAuthSync(authSyncTransportBot.bot, authSync, config, logger);
900
- authSync.start();
901
- void publishPendingClusterUpdateBroadcast(authSync, logger);
739
+ const runtimeConfig = {
740
+ ...config,
741
+ tgBotToken: token,
742
+ tgBotTokens: [token],
743
+ tgScopeBotId: id,
744
+ codexAuthDir,
745
+ codexHome,
746
+ antigravityHome,
747
+ antigravityAuthDir,
748
+ codexAppAutolaunch,
749
+ codexAppServerStatePath,
750
+ codexAppServerLogPath,
751
+ };
752
+ if (namespacedScopes && (isCodexDefault || isAntigravityDefault)) {
753
+ store.migrateTelegramUnnamespacedScope(id, config.tgAllowedUserId);
902
754
  }
903
- mirror.start();
904
- process.on('unhandledRejection', (error) => {
905
- logger.error('process.unhandled_rejection', { error: serializeError(error) });
755
+ seeds.push({
756
+ id,
757
+ username,
758
+ home: codexHome,
759
+ authDir: codexAuthDir,
760
+ sharedDefaultRuntime: isCodexDefault || isAntigravityDefault,
761
+ config: runtimeConfig,
762
+ bot,
763
+ app: codexApp,
764
+ antigravityApp,
765
+ antigravityAuth,
766
+ antigravityHome,
767
+ antigravityAuthDir,
768
+ defaultBackend: backend,
906
769
  });
907
- process.on('uncaughtException', (error) => {
908
- logger.error('process.uncaught_exception', { error: serializeError(error) });
909
- });
910
- for (const runtime of runtimes) {
911
- await runtime.telegram.start();
912
- }
913
- if (weixinAdapter) {
914
- await activeWeixinCore.startCodexApp();
915
- await weixinAdapter.start();
916
- }
917
- writeAggregateStatus();
918
- logger.info('bridge.started', { bots: runtimes.map((runtime) => runtime.id) });
919
- const shutdown = async (signal) => {
920
- logger.info('bridge.shutting_down', { signal });
921
- await authNotificationAggregator.flushAll();
922
- authSync?.stop();
923
- mirror.stop();
924
- await weixinAdapter?.stop();
925
- await activeWeixinCore?.stop();
926
- await Promise.all(runtimes.map((runtime) => runtime.telegram.stop()));
927
- await activeOpencodeRuntime?.stop();
928
- writeAggregateStatus(false);
929
- await Promise.all(managedApps.map((app) => app.stop({ terminateServer: true }).catch((error) => {
930
- logger.warn('codex.app-server.stop_failed', { error: serializeError(error) });
931
- })));
932
- store?.close();
933
- processLock.release();
934
- process.exit(0);
935
- };
936
- process.on('SIGINT', () => void shutdown('SIGINT'));
937
- process.on('SIGTERM', () => void shutdown('SIGTERM'));
938
- return;
939
770
  }
940
- const singleCodexHome = config.codexHome ?? path.join(os.homedir(), '.codex');
941
- installBundledCodexSkills(packageRoot, singleCodexHome);
942
- const bot = new TelegramGateway(config.tgBotToken, config.tgAllowedUserId, config.tgAllowedChatId, config.telegramPollIntervalMs, store, logger);
943
- const app = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger, config.codexHome ? { CODEX_HOME: config.codexHome } : null, codexApiProviderOverrides);
944
- const telegramMessaging = new TelegramMessagingPort(bot);
945
- const weixinMessaging = config.wxEnabled
946
- ? new WeixinMessagingPort(store, (id) => loadWeixinAccount(config.weixinAccountsDir, id))
947
- : null;
948
- const outbound = new BridgeMessagingRouter(telegramMessaging, weixinMessaging);
949
- const selfUpdater = createSelfUpdateRuntime({
950
- entryPoint,
951
- nodePath: process.execPath,
952
- version: readPackageVersion(),
953
- statusPath: config.statusPath,
954
- logPath: path.join(APP_HOME, 'logs', 'update.log'),
955
- codexCliBin: config.codexCliBin,
771
+ let authSync = null;
772
+ const authRuntimeIds = seeds.map((runtime) => runtime.id);
773
+ const mirror = new AuthCandidateMirror(canonicalAuthDir, seeds.map((runtime) => ({
774
+ id: runtime.id,
775
+ label: runtime.bot.username ? `@${runtime.bot.username}` : runtime.id,
776
+ authDir: runtime.authDir,
777
+ validate: async (context) => validateRefreshedAuthCandidate(runtime, context.candidateName),
778
+ notify: createAuthMirrorNotifier(store, runtime.id, runtime.bot, authNotificationAggregator, {
779
+ quietAuthPoolMode: () => config.authAutoDeleteNeedsRepair,
780
+ suppressBackgroundNotifications: () => true,
781
+ }),
782
+ })), logger, path.join(APP_HOME, 'runtime', 'auth-mirror.json'), {
783
+ onSynced: async (event) => {
784
+ await authSync?.publishCandidate(event.record.candidateName);
785
+ },
786
+ onRemoteImported: (event) => {
787
+ restoreImportedAuthCandidateState(store, event.record.candidateName, authRuntimeIds);
788
+ },
956
789
  });
957
- let singleAuthSync = null;
958
- let singleMirror = null;
959
- let core = null;
960
- const singleAuthDir = config.codexAuthDir ?? config.codexHome ?? process.env.CODEX_AUTH_DIR ?? path.join(os.homedir(), '.codex');
961
- const singleLocalAuthRefreshLease = createLocalAuthRefreshLease();
962
- const singleAuthSyncLocalIdle = () => Boolean(core?.isIdleForServiceUpdate())
963
- && (!singleMirror || singleMirror.isIdle());
964
- const singleClusterUpdateScheduler = createClusterUpdateScheduler({
790
+ await mirror.initialize();
791
+ activeAuthMirror = mirror;
792
+ managedApps = seeds.map((runtime) => runtime.app);
793
+ const lastSelfUpdatePath = path.join(APP_HOME, 'runtime', 'last-self-update.json');
794
+ let lastSelfUpdate = readSelfUpdateStatus(lastSelfUpdatePath);
795
+ const runtimes = [];
796
+ const localAuthRefreshLease = createLocalAuthRefreshLease();
797
+ const writeAggregateStatus = (running = true) => {
798
+ const statuses = runtimes.map((runtime) => runtime.core.getRuntimeStatus());
799
+ const weixinStatus = activeWeixinCore?.getRuntimeStatus() ?? null;
800
+ const authProactiveRefresh = newestAuthProactiveRefreshStatus([...statuses, ...(weixinStatus ? [weixinStatus] : [])]);
801
+ const first = statuses[0] ?? null;
802
+ writeRuntimeStatus(config.statusPath, {
803
+ running,
804
+ connected: running
805
+ && statuses.every((status) => status.connected)
806
+ && (!weixinStatus || weixinStatus.connected),
807
+ userAgent: first?.userAgent ?? null,
808
+ codexHome: first?.codexHome ?? null,
809
+ ...(first?.codexAppServer ? { codexAppServer: first.codexAppServer } : {}),
810
+ botUsername: first?.botUsername ?? null,
811
+ currentBindings: store.countBindings(),
812
+ pendingApprovals: store.countPendingApprovals(),
813
+ pendingUserInputs: store.countPendingUserInputs(),
814
+ queuedTurns: store.countQueuedTurnInputs(),
815
+ activeTurns: statuses.reduce((sum, status) => sum + status.activeTurns, 0)
816
+ + (weixinStatus?.activeTurns ?? 0),
817
+ lastError: statuses.find((status) => status.lastError)?.lastError
818
+ ?? weixinStatus?.lastError
819
+ ?? null,
820
+ updatedAt: new Date().toISOString(),
821
+ channels: { telegram: running, weixin: running && config.wxEnabled },
822
+ bots: runtimes.map((runtime, index) => ({
823
+ id: runtime.id,
824
+ username: statuses[index]?.botUsername ?? runtime.bot.username,
825
+ connected: running && Boolean(statuses[index]?.connected),
826
+ activeTurns: running ? (statuses[index]?.activeTurns ?? 0) : 0,
827
+ runtimeKind: runtime.sharedDefaultRuntime ? 'default' : 'isolated',
828
+ defaultBackend: runtime.defaultBackend,
829
+ codexHome: statuses[index]?.codexHome ?? runtime.home,
830
+ antigravityHome: runtime.antigravityHome,
831
+ ...(statuses[index]?.codexAppServer ? { codexAppServer: statuses[index].codexAppServer } : {}),
832
+ })),
833
+ ...(weixinStatus ? {
834
+ weixinRuntime: {
835
+ connected: running && weixinStatus.connected,
836
+ activeTurns: running ? weixinStatus.activeTurns : 0,
837
+ ...(weixinStatus.codexAppServer ? { codexAppServer: weixinStatus.codexAppServer } : {}),
838
+ },
839
+ } : {}),
840
+ authMirror: mirror.getStatus(),
841
+ authSync: authSync?.getStatus() ?? null,
842
+ authProactiveRefresh,
843
+ lastUpdate: lastSelfUpdate,
844
+ });
845
+ };
846
+ const authSyncLocalIdle = () => runtimes.every((runtime) => runtime.core.isIdleForServiceUpdate())
847
+ && (!activeWeixinCore || activeWeixinCore.isIdleForServiceUpdate())
848
+ && mirror.isIdle();
849
+ const clusterUpdateScheduler = createClusterUpdateScheduler({
965
850
  selfUpdater,
966
- canUpdate: () => singleAuthSyncLocalIdle()
967
- && (singleAuthSync ? singleAuthSync.isIdle() : singleLocalAuthRefreshLease.isIdle()),
851
+ canUpdate: () => authSyncLocalIdle()
852
+ && (authSync ? authSync.isIdle() : localAuthRefreshLease.isIdle()),
968
853
  currentVersion: readPackageVersion,
969
854
  logger,
970
855
  });
971
- const singleCoordinator = config.authSyncEnabled ? {
972
- canSelfUpdate: () => singleAuthSyncLocalIdle()
973
- && (singleAuthSync ? singleAuthSync.isIdle() : singleLocalAuthRefreshLease.isIdle()),
974
- authCandidateUpdated: (runtimeId, candidateName) => singleMirror?.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined) ?? Promise.resolve(),
856
+ const coordinator = {
857
+ canSelfUpdate: () => authSyncLocalIdle()
858
+ && (authSync ? authSync.isIdle() : localAuthRefreshLease.isIdle()),
859
+ authCandidateUpdated: (runtimeId, candidateName) => mirror.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined),
975
860
  authCandidateDeleted: async (_runtimeId, candidateName, reason = null) => {
976
- await singleMirror?.deleteCandidate(candidateName);
977
- await singleAuthSync?.publishCandidateDeletion(candidateName, reason);
861
+ await mirror.deleteCandidate(candidateName);
862
+ await authSync?.publishCandidateDeletion(candidateName, reason);
978
863
  },
979
864
  recoverAuthCandidate: async (runtimeId, candidateName, options = {}) => {
980
- const local = await singleMirror?.recoverRuntimeCandidate(runtimeId, candidateName) ?? null;
865
+ const local = await mirror.recoverRuntimeCandidate(runtimeId, candidateName);
981
866
  if (local)
982
867
  return true;
983
868
  if (options.crossNode === false)
984
869
  return false;
985
- const current = await singleMirror?.readRuntimeCandidate(runtimeId, candidateName)
986
- ?? await singleMirror?.readNewestCandidate(candidateName)
987
- ?? null;
988
- return await singleAuthSync?.requestRecovery(candidateName, {
870
+ const current = await mirror.readRuntimeCandidate(runtimeId, candidateName)
871
+ ?? await mirror.readNewestCandidate(candidateName);
872
+ return await authSync?.requestRecovery(candidateName, {
989
873
  accountId: current?.accountId ?? null,
990
874
  quotaIdentityId: current?.quotaIdentityId ?? null,
991
875
  lastRefreshMs: current?.lastRefreshMs ?? null,
992
876
  }) ?? false;
993
877
  },
994
- acquireAuthRefreshLease: (reason) => singleAuthSync?.acquireRefreshLease(reason)
995
- ?? singleLocalAuthRefreshLease.acquire(reason),
996
- releaseAuthRefreshLease: (leaseId) => singleAuthSync?.releaseRefreshLease(leaseId)
997
- ?? singleLocalAuthRefreshLease.release(leaseId),
998
- getAuthSyncStatus: () => singleAuthSync?.getStatus() ?? null,
878
+ acquireAuthRefreshLease: (reason) => authSync?.acquireRefreshLease(reason)
879
+ ?? localAuthRefreshLease.acquire(reason),
880
+ releaseAuthRefreshLease: (leaseId) => authSync?.releaseRefreshLease(leaseId)
881
+ ?? localAuthRefreshLease.release(leaseId),
882
+ getAuthSyncStatus: () => authSync?.getStatus() ?? null,
999
883
  authSyncSafeAll: async () => {
1000
- const local = await singleMirror?.syncAllRuntimeCandidates() ?? { synced: 0, skipped: 0 };
1001
- const remote = await singleAuthSync?.pushAll() ?? { sent: 0, skipped: 0 };
884
+ const local = await mirror.syncAllRuntimeCandidates();
885
+ const remote = await authSync?.pushAll() ?? { sent: 0, skipped: 0 };
1002
886
  return {
1003
887
  localSynced: local.synced,
1004
888
  localSkipped: local.skipped,
@@ -1006,124 +890,148 @@ async function runServeCli() {
1006
890
  skipped: remote.skipped,
1007
891
  };
1008
892
  },
1009
- authSyncPushAll: () => singleAuthSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
1010
- authSyncTest: () => singleAuthSync?.testPeers() ?? Promise.resolve({ sent: 0, replied: 0, missing: [] }),
1011
- authSyncAudit: () => singleAuthSync?.auditCluster() ?? Promise.resolve(null),
1012
- statusUpdated: (status) => {
1013
- writeRuntimeStatus(config.statusPath, {
1014
- ...status,
1015
- authMirror: singleMirror?.getStatus() ?? null,
1016
- authSync: singleAuthSync?.getStatus() ?? null,
1017
- });
893
+ authSyncPushAll: () => authSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
894
+ authSyncTest: () => authSync?.testPeers() ?? Promise.resolve({ sent: 0, replied: 0, missing: [] }),
895
+ authSyncAudit: () => authSync?.auditCluster() ?? Promise.resolve(null),
896
+ statusUpdated: () => writeAggregateStatus(),
897
+ getServiceStatus: async () => ({
898
+ currentVersion: readPackageVersion(),
899
+ bots: await Promise.all(runtimes.map(async (runtime) => {
900
+ const status = runtime.core.getRuntimeStatus();
901
+ return {
902
+ id: runtime.id,
903
+ username: status.botUsername ?? runtime.bot.username,
904
+ connected: status.connected,
905
+ activeTurns: status.activeTurns,
906
+ runtimeKind: runtime.sharedDefaultRuntime ? 'default' : 'isolated',
907
+ currentAuth: await runtime.core.getCurrentAuthLabel().catch(() => null),
908
+ codexHome: status.codexHome ?? runtime.home,
909
+ ...(status.codexAppServer ? { codexAppServer: status.codexAppServer } : {}),
910
+ };
911
+ })),
912
+ ...(activeWeixinCore ? {
913
+ weixinRuntime: {
914
+ connected: activeWeixinCore.getRuntimeStatus().connected,
915
+ activeTurns: activeWeixinCore.getRuntimeStatus().activeTurns,
916
+ codexAppServer: activeWeixinCore.getRuntimeStatus().codexAppServer,
917
+ },
918
+ } : {}),
919
+ authMirror: mirror.getStatus(),
920
+ authSync: authSync?.getStatus() ?? null,
921
+ authProactiveRefresh: newestAuthProactiveRefreshStatus([
922
+ ...runtimes.map(runtime => runtime.core.getRuntimeStatus()),
923
+ ...(activeWeixinCore ? [activeWeixinCore.getRuntimeStatus()] : []),
924
+ ]),
925
+ lastUpdate: lastSelfUpdate,
926
+ }),
927
+ selfUpdateCompleted: (status) => {
928
+ lastSelfUpdate = status;
929
+ writeSelfUpdateStatus(lastSelfUpdatePath, status);
930
+ writeAggregateStatus();
1018
931
  },
1019
- } : null;
1020
- if (config.authSyncEnabled) {
1021
- singleMirror = new AuthCandidateMirror(singleAuthDir, [{
1022
- id: 'default',
1023
- label: bot.username ? `@${bot.username}` : 'default',
1024
- authDir: singleAuthDir,
1025
- validate: async (context) => validateRefreshedAuthCandidate({
1026
- id: 'default',
1027
- authDir: singleAuthDir,
1028
- app,
1029
- }, context.candidateName),
1030
- }], logger, path.join(APP_HOME, 'runtime', 'auth-mirror.json'), {
1031
- onSynced: async (event) => {
1032
- await singleAuthSync?.publishCandidate(event.record.candidateName);
1033
- },
1034
- onRemoteImported: (event) => {
1035
- restoreImportedAuthCandidateState(store, event.record.candidateName, ['default']);
1036
- },
932
+ };
933
+ for (const seed of seeds) {
934
+ const telegramMessaging = new TelegramMessagingPort(seed.bot);
935
+ const outbound = new BridgeMessagingRouter(telegramMessaging, null);
936
+ const codexCore = new BridgeSessionCore(seed.config, store, logger, seed.bot, seed.app, outbound, selfUpdater, coordinator, false);
937
+ const core = new UnifiedBridgeCore(seed.config, store, logger, seed.bot, seed.antigravityApp, seed.antigravityAuth, telegramMessaging, {
938
+ codexApp: seed.app,
939
+ codexCore,
940
+ antigravityApp: seed.antigravityApp,
941
+ antigravityAuth: seed.antigravityAuth,
942
+ defaultBackendId: seed.defaultBackend,
943
+ selfUpdater,
1037
944
  });
1038
- await singleMirror.initialize();
1039
- activeAuthMirror = singleMirror;
945
+ runtimes.push({ ...seed, core, telegram: new TelegramChannelAdapter(core) });
946
+ }
947
+ if (config.wxEnabled) {
948
+ const weixinApp = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger, null, codexApiProviderOverrides);
949
+ const outbound = new BridgeMessagingRouter(new TelegramMessagingPort(seeds[0].bot), new WeixinMessagingPort(store, (id) => loadWeixinAccount(config.weixinAccountsDir, id)));
950
+ activeWeixinCore = new BridgeSessionCore(config, store, logger, seeds[0].bot, weixinApp, outbound, selfUpdater, coordinator, false);
951
+ managedApps.push(weixinApp);
952
+ weixinAdapter = new WeixinChannelAdapter(activeWeixinCore, store, config, logger);
1040
953
  }
1041
- core = new BridgeSessionCore(config, store, logger, bot, app, outbound, selfUpdater, singleCoordinator);
1042
- if (config.authSyncEnabled && singleMirror) {
1043
- await bot.initializeIdentity();
1044
- singleAuthSync = new CrossNodeAuthSync(buildAuthSyncConfig(config, bot.username ? `@${bot.username}` : 'default'), logger, {
954
+ activeTelegramAdapters = runtimes.map((runtime) => runtime.telegram);
955
+ if (config.authSyncEnabled) {
956
+ const authSyncTransportBot = seeds[0];
957
+ const authSyncTransportLabel = authSyncTransportBot.bot.username
958
+ ? `@${authSyncTransportBot.bot.username}`
959
+ : authSyncTransportBot.id;
960
+ authSync = new CrossNodeAuthSync(buildAuthSyncConfig(config, authSyncTransportLabel), logger, {
1045
961
  send: async (peer, envelope) => {
1046
- await bot.sendDocument(peer, `foxclaw-auth-sync-${Date.now()}.json`, Buffer.from(envelope, 'utf8'), 'FOXCLAW_AUTH_SYNC_V1');
962
+ await authSyncTransportBot.bot.sendDocument(peer, `foxclaw-auth-sync-${Date.now()}.json`, Buffer.from(envelope, 'utf8'), 'FOXCLAW_AUTH_SYNC_V1');
1047
963
  },
1048
964
  }, {
1049
- readLocalCandidate: (candidateName) => singleMirror.readNewestCandidate(candidateName),
1050
- listLocalCandidates: () => singleMirror.listNewestCandidates(),
1051
- listLocalCandidateCopies: () => singleMirror.listCandidateCopies(),
965
+ readLocalCandidate: (candidateName) => mirror.readNewestCandidate(candidateName),
966
+ listLocalCandidates: () => mirror.listNewestCandidates(),
967
+ listLocalCandidateCopies: () => mirror.listCandidateCopies(),
1052
968
  validateCandidate: async (candidateName, raw, expectedAccountId) => {
1053
- if (!singleAuthSyncLocalIdle()) {
969
+ if (!authSyncLocalIdle()) {
1054
970
  return { ok: false, reason: 'runtime is not idle' };
1055
971
  }
1056
- return core.validateExternalCodexAuthCandidate(candidateName, raw, expectedAccountId);
972
+ const runtime = runtimes.find((entry) => entry.core.isIdleForServiceUpdate()) ?? runtimes[0] ?? null;
973
+ if (!runtime) {
974
+ return { ok: false, reason: 'no validation runtime is available' };
975
+ }
976
+ return runtime.core.validateExternalCodexAuthCandidate(candidateName, raw, expectedAccountId);
1057
977
  },
1058
- importCandidate: (candidateName, raw, source) => singleMirror.importExternalCandidate(candidateName, raw, source),
1059
- markCandidateState: (candidateName, state, expected) => markAuditedAuthCandidateState(store, singleMirror, candidateName, state, expected, ['default']),
978
+ importCandidate: (candidateName, raw, source) => mirror.importExternalCandidate(candidateName, raw, source),
979
+ markCandidateState: (candidateName, state, expected) => markAuditedAuthCandidateState(store, mirror, candidateName, state, expected, authRuntimeIds),
1060
980
  deleteLocalCandidate: async (candidateName, source) => {
1061
- if (!singleAuthSyncLocalIdle()) {
981
+ if (!authSyncLocalIdle()) {
1062
982
  return { ok: false, deleted: false, reason: 'runtime is not idle' };
1063
983
  }
1064
- await singleMirror.deleteCandidate(candidateName);
984
+ await mirror.deleteCandidate(candidateName);
1065
985
  store.deleteCodexAuthCandidate(candidateName);
1066
- await core.handleExternalCodexAuthCandidateDeleted(candidateName, source.reason ?? null);
986
+ await Promise.all(runtimes.map(runtime => runtime.core.handleExternalCodexAuthCandidateDeleted(candidateName, source.reason ?? null)));
987
+ if (activeWeixinCore) {
988
+ await activeWeixinCore.handleExternalCodexAuthCandidateDeleted(candidateName, source.reason ?? null);
989
+ }
1067
990
  return { ok: true, deleted: true, reason: source.reason ?? null };
1068
991
  },
1069
- scheduleServiceUpdate: (source) => singleClusterUpdateScheduler.schedule(source),
1070
- isIdle: singleAuthSyncLocalIdle,
1071
- notify: createAuthSyncNotifier(store, bot, authNotificationAggregator, {
992
+ scheduleServiceUpdate: (source) => clusterUpdateScheduler.schedule(source),
993
+ isIdle: authSyncLocalIdle,
994
+ notify: createAuthSyncNotifier(store, authSyncTransportBot.bot, authNotificationAggregator, {
1072
995
  quietAuthPoolMode: () => config.authAutoDeleteNeedsRepair,
1073
996
  suppressBackgroundNotifications: () => true,
1074
997
  }),
1075
998
  });
1076
- await singleAuthSync.initialize();
1077
- activeAuthSync = singleAuthSync;
1078
- attachTelegramAuthSync(bot, singleAuthSync, config, logger);
1079
- singleAuthSync.start();
1080
- void publishPendingClusterUpdateBroadcast(singleAuthSync, logger);
1081
- singleMirror.start();
1082
- }
1083
- const telegram = new TelegramChannelAdapter(core);
1084
- managedApps = [app];
1085
- activeTelegramAdapters = [telegram];
1086
- if (config.wxEnabled) {
1087
- weixinAdapter = new WeixinChannelAdapter(core, store, config, logger);
999
+ await authSync.initialize();
1000
+ activeAuthSync = authSync;
1001
+ attachTelegramAuthSync(authSyncTransportBot.bot, authSync, config, logger);
1002
+ authSync.start();
1003
+ void publishPendingClusterUpdateBroadcast(authSync, logger);
1088
1004
  }
1005
+ mirror.start();
1089
1006
  process.on('unhandledRejection', (error) => {
1090
1007
  logger.error('process.unhandled_rejection', { error: serializeError(error) });
1091
1008
  });
1092
1009
  process.on('uncaughtException', (error) => {
1093
1010
  logger.error('process.uncaught_exception', { error: serializeError(error) });
1094
1011
  });
1095
- await telegram.start();
1012
+ for (const runtime of runtimes) {
1013
+ await runtime.telegram.start();
1014
+ }
1096
1015
  if (weixinAdapter) {
1016
+ await activeWeixinCore.startCodexApp();
1097
1017
  await weixinAdapter.start();
1098
1018
  }
1099
- logger.info('bridge.started', core.getRuntimeStatus());
1019
+ writeAggregateStatus();
1020
+ logger.info('bridge.started', { bots: runtimes.map((runtime) => runtime.id) });
1100
1021
  const shutdown = async (signal) => {
1101
1022
  logger.info('bridge.shutting_down', { signal });
1102
1023
  await authNotificationAggregator.flushAll();
1103
- singleAuthSync?.stop();
1104
- singleMirror?.stop();
1024
+ authSync?.stop();
1025
+ mirror.stop();
1105
1026
  await weixinAdapter?.stop();
1106
- await telegram.stop();
1027
+ await activeWeixinCore?.stop();
1028
+ await Promise.all(runtimes.map((runtime) => runtime.telegram.stop()));
1107
1029
  await activeOpencodeRuntime?.stop();
1108
- writeRuntimeStatus(config.statusPath, {
1109
- running: false,
1110
- connected: false,
1111
- userAgent: app.getUserAgent(),
1112
- codexHome: singleCodexHome,
1113
- codexAppServer: app.getServerStatus(),
1114
- botUsername: bot.username,
1115
- currentBindings: 0,
1116
- pendingApprovals: 0,
1117
- pendingUserInputs: 0,
1118
- queuedTurns: 0,
1119
- activeTurns: 0,
1120
- lastError: null,
1121
- updatedAt: new Date().toISOString(),
1122
- channels: { telegram: false, weixin: false },
1123
- });
1124
- await app.stop({ terminateServer: true }).catch((error) => {
1030
+ writeAggregateStatus(false);
1031
+ const allManaged = [...managedApps, ...(sharedCodexApp ? [sharedCodexApp] : [])];
1032
+ await Promise.all(allManaged.map((app) => app.stop({ terminateServer: true }).catch((error) => {
1125
1033
  logger.warn('codex.app-server.stop_failed', { error: serializeError(error) });
1126
- });
1034
+ })));
1127
1035
  store?.close();
1128
1036
  processLock.release();
1129
1037
  process.exit(0);
@@ -1599,15 +1507,22 @@ async function configureEnvInteractively(envPath, existed) {
1599
1507
  const skipped = [];
1600
1508
  const warnings = [];
1601
1509
  Object.assign(updates, await maybeSaveProxyEnvFromShell(rl, envPath));
1602
- const tokens = sanitizeEnvInput(await rl.question('Telegram bot token(s), comma-separated (TG_BOT_TOKENS): '));
1603
- if (tokens) {
1604
- updates.TG_BOT_TOKENS = tokens;
1605
- if (tokens.split(',').map((token) => token.trim()).some((token) => !/^\d+:[A-Za-z0-9_-]+$/.test(token))) {
1606
- warnings.push('One or more TG_BOT_TOKENS values do not look like standard Telegram bot tokens.');
1510
+ const codexTokens = sanitizeEnvInput(await rl.question('Codex Telegram bot token(s), comma-separated (CODEX_BOT_TOKENS): '));
1511
+ if (codexTokens) {
1512
+ updates.CODEX_BOT_TOKENS = codexTokens;
1513
+ if (codexTokens.split(',').map((token) => token.trim()).some((token) => !/^\d+:[A-Za-z0-9_-]+$/.test(token))) {
1514
+ warnings.push('One or more CODEX_BOT_TOKENS values do not look like standard Telegram bot tokens.');
1607
1515
  }
1608
1516
  }
1609
1517
  else {
1610
- skipped.push('TG_BOT_TOKENS');
1518
+ skipped.push('CODEX_BOT_TOKENS');
1519
+ }
1520
+ const agyTokens = sanitizeEnvInput(await rl.question('Antigravity Telegram bot token(s), comma-separated (ANTIGRAVITY_BOT_TOKENS) [optional]: '));
1521
+ if (agyTokens) {
1522
+ updates.ANTIGRAVITY_BOT_TOKENS = agyTokens;
1523
+ if (agyTokens.split(',').map((token) => token.trim()).some((token) => !/^\d+:[A-Za-z0-9_-]+$/.test(token))) {
1524
+ warnings.push('One or more ANTIGRAVITY_BOT_TOKENS values do not look like standard Telegram bot tokens.');
1525
+ }
1611
1526
  }
1612
1527
  const userId = sanitizeEnvInput(await rl.question('Telegram numeric user ID (TG_ALLOWED_USER_ID): '));
1613
1528
  if (userId) {
@@ -1818,20 +1733,34 @@ function stopService() {
1818
1733
  }
1819
1734
  function runDoctorChecks() {
1820
1735
  const configuredCodexBin = process.env.CODEX_CLI_BIN;
1736
+ const codexTokens = [
1737
+ ...(process.env.CODEX_BOT_TOKENS ?? '').split(','),
1738
+ process.env.CODEX_BOT_TOKEN ?? '',
1739
+ ].map((value) => value.trim()).filter(Boolean);
1740
+ const antigravityTokens = [
1741
+ ...(process.env.ANTIGRAVITY_BOT_TOKENS ?? '').split(','),
1742
+ process.env.ANTIGRAVITY_BOT_TOKEN ?? '',
1743
+ ].map((value) => value.trim()).filter(Boolean);
1821
1744
  const checks = [
1822
1745
  ['node >= 24', Number(process.versions.node.split('.')[0]) >= 24],
1823
1746
  ['codex cli available', hasConfiguredCommand(configuredCodexBin, 'codex')],
1824
- ['telegram bot token(s) configured', Boolean(process.env.TG_BOT_TOKENS?.trim() || process.env.TG_BOT_TOKEN?.trim())],
1747
+ ['telegram bot token(s) configured', codexTokens.length > 0 || antigravityTokens.length > 0],
1825
1748
  ['telegram allowed user configured', Boolean(process.env.TG_ALLOWED_USER_ID)],
1826
1749
  ];
1827
1750
  if (process.env.OPENCODE_BOT_TOKEN?.trim()) {
1828
1751
  const configuredOpencodeBin = process.env.OPENCODE_CLI_BIN;
1829
1752
  checks.push(['opencode cli available', hasConfiguredCommand(configuredOpencodeBin, 'opencode')]);
1830
- const codexTokens = [
1831
- ...(process.env.TG_BOT_TOKENS ?? '').split(','),
1832
- process.env.TG_BOT_TOKEN ?? '',
1833
- ].map((value) => value.trim()).filter(Boolean);
1834
- checks.push(['OpenCode uses an independent Telegram bot', !codexTokens.includes(process.env.OPENCODE_BOT_TOKEN.trim())]);
1753
+ const allOtherTokens = [...codexTokens, ...antigravityTokens];
1754
+ checks.push(['OpenCode uses an independent Telegram bot', !allOtherTokens.includes(process.env.OPENCODE_BOT_TOKEN.trim())]);
1755
+ }
1756
+ if (antigravityTokens.length > 0) {
1757
+ const configuredAgyBin = process.env.ANTIGRAVITY_CLI_BIN;
1758
+ checks.push(['antigravity (agy) cli available', hasConfiguredCommand(configuredAgyBin, 'agy')]);
1759
+ const hasConflict = antigravityTokens.some((t) => codexTokens.includes(t));
1760
+ checks.push(['Antigravity uses independent Telegram bot(s)', !hasConflict]);
1761
+ const authDir = process.env.ANTIGRAVITY_AUTH_DIR || path.join(os.homedir(), '.gemini', 'antigravity-cli');
1762
+ const hasAuth = fs.existsSync(path.join(authDir, 'antigravity-oauth-token'));
1763
+ checks.push(['antigravity oauth token available', hasAuth]);
1835
1764
  }
1836
1765
  if (process.env.WX_ENABLED === 'true' || process.env.WX_ENABLED === '1') {
1837
1766
  const accountsDir = process.env.WEIXIN_ACCOUNTS_DIR || path.join(APP_HOME, 'weixin', 'accounts');