@foxden-app/foxclaw 0.11.1 → 0.13.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 (138) hide show
  1. package/.env.example +18 -1
  2. package/CHANGELOG.md +36 -0
  3. package/README.md +4 -3
  4. package/README_EN.md +4 -3
  5. package/dist/antigravity/account_ui.d.ts +34 -0
  6. package/dist/antigravity/account_ui.js +354 -0
  7. package/dist/antigravity/adapter.js +1 -1
  8. package/dist/antigravity/auth.d.ts +4 -1
  9. package/dist/antigravity/auth.js +17 -5
  10. package/dist/antigravity/controller.d.ts +3 -106
  11. package/dist/antigravity/controller.js +2 -2083
  12. package/dist/antigravity/conversation_ui.d.ts +56 -0
  13. package/dist/antigravity/conversation_ui.js +509 -0
  14. package/dist/antigravity/ui_callbacks.d.ts +6 -0
  15. package/dist/antigravity/ui_callbacks.js +6 -0
  16. package/dist/bridge/unified_bridge.d.ts +114 -0
  17. package/dist/bridge/unified_bridge.js +1343 -0
  18. package/dist/channels/bridge_messaging_router.d.ts +8 -4
  19. package/dist/channels/bridge_messaging_router.js +15 -4
  20. package/dist/channels/telegram/telegram_messaging_port.d.ts +16 -0
  21. package/dist/channels/telegram/telegram_messaging_port.js +22 -2
  22. package/dist/channels/text_actions.d.ts +14 -0
  23. package/dist/channels/text_actions.js +48 -0
  24. package/dist/channels/weixin/weixin_channel_adapter.d.ts +3 -4
  25. package/dist/channels/weixin/weixin_channel_adapter.js +2 -2
  26. package/dist/channels/weixin/weixin_messaging_port.d.ts +16 -1
  27. package/dist/channels/weixin/weixin_messaging_port.js +34 -7
  28. package/dist/codex_app/activity.d.ts +62 -0
  29. package/dist/codex_app/activity.js +418 -0
  30. package/dist/codex_app/adapter.d.ts +2 -2
  31. package/dist/codex_app/adapter.js +55 -25
  32. package/dist/codex_app/backend_ui.d.ts +4 -0
  33. package/dist/codex_app/backend_ui.js +48 -0
  34. package/dist/codex_app/client.js +0 -12
  35. package/dist/config.d.ts +4 -0
  36. package/dist/config.js +41 -5
  37. package/dist/controller/access.d.ts +1 -11
  38. package/dist/controller/access.js +1 -33
  39. package/dist/controller/activity.d.ts +1 -62
  40. package/dist/controller/activity.js +2 -418
  41. package/dist/controller/auth_errors.d.ts +11 -0
  42. package/dist/controller/auth_errors.js +124 -0
  43. package/dist/controller/auth_files.d.ts +13 -0
  44. package/dist/controller/auth_files.js +156 -0
  45. package/dist/controller/auth_presentation.d.ts +77 -0
  46. package/dist/controller/auth_presentation.js +697 -0
  47. package/dist/controller/auth_quota_service.d.ts +27 -0
  48. package/dist/controller/auth_quota_service.js +151 -0
  49. package/dist/controller/controller.d.ts +30 -56
  50. package/dist/controller/controller.js +181 -3497
  51. package/dist/controller/interactions.d.ts +74 -0
  52. package/dist/controller/interactions.js +560 -0
  53. package/dist/controller/local_usage_service.d.ts +19 -0
  54. package/dist/controller/local_usage_service.js +62 -0
  55. package/dist/controller/native_panel_service.d.ts +55 -0
  56. package/dist/controller/native_panel_service.js +214 -0
  57. package/dist/controller/native_panels.d.ts +61 -0
  58. package/dist/controller/native_panels.js +784 -0
  59. package/dist/controller/shared_helpers.d.ts +53 -0
  60. package/dist/controller/shared_helpers.js +399 -0
  61. package/dist/controller/state_types.d.ts +390 -0
  62. package/dist/controller/state_types.js +82 -0
  63. package/dist/controller/turn_rendering.d.ts +22 -0
  64. package/dist/controller/turn_rendering.js +288 -0
  65. package/dist/core/access.d.ts +11 -0
  66. package/dist/core/access.js +33 -0
  67. package/dist/core/attachment_files.d.ts +13 -0
  68. package/dist/core/attachment_files.js +180 -0
  69. package/dist/core/attachment_types.d.ts +22 -0
  70. package/dist/core/attachment_types.js +1 -0
  71. package/dist/core/attachments.d.ts +5 -3
  72. package/dist/core/attachments.js +2 -2
  73. package/dist/core/backend_registry.d.ts +22 -0
  74. package/dist/core/backend_registry.js +50 -0
  75. package/dist/core/backend_ui.d.ts +28 -0
  76. package/dist/core/backend_ui.js +1 -0
  77. package/dist/core/channel_events.d.ts +48 -0
  78. package/dist/core/channel_events.js +1 -0
  79. package/dist/core/channel_port.d.ts +52 -8
  80. package/dist/core/engine_spi.d.ts +20 -5
  81. package/dist/core/listener_scope.d.ts +11 -0
  82. package/dist/core/listener_scope.js +10 -0
  83. package/dist/core/message_text.d.ts +2 -0
  84. package/dist/core/message_text.js +28 -0
  85. package/dist/core/network_errors.d.ts +1 -0
  86. package/dist/core/network_errors.js +23 -0
  87. package/dist/core/orchestrator.d.ts +57 -22
  88. package/dist/core/orchestrator.js +877 -477
  89. package/dist/core/queued_engine_input.d.ts +12 -0
  90. package/dist/core/queued_engine_input.js +16 -0
  91. package/dist/core/runtime_supervisor.d.ts +19 -0
  92. package/dist/core/runtime_supervisor.js +45 -0
  93. package/dist/core/scope_operations.d.ts +7 -0
  94. package/dist/core/scope_operations.js +19 -0
  95. package/dist/core/stream_preview.js +21 -9
  96. package/dist/dsh/adapter.d.ts +48 -0
  97. package/dist/dsh/adapter.js +205 -0
  98. package/dist/dsh/backend.d.ts +7 -0
  99. package/dist/dsh/backend.js +21 -0
  100. package/dist/dsh/client.d.ts +46 -0
  101. package/dist/dsh/client.js +184 -0
  102. package/dist/dsh/permissions_plugin.d.ts +30 -0
  103. package/dist/dsh/permissions_plugin.js +21 -0
  104. package/dist/dsh/runtime.d.ts +20 -0
  105. package/dist/dsh/runtime.js +36 -0
  106. package/dist/dsh/ui.d.ts +38 -0
  107. package/dist/dsh/ui.js +342 -0
  108. package/dist/i18n.d.ts +4 -0
  109. package/dist/i18n.js +12 -0
  110. package/dist/main.js +144 -72
  111. package/dist/opencode/adapter.js +28 -9
  112. package/dist/opencode/controller.js +1 -1
  113. package/dist/store/channel_inbox.d.ts +14 -0
  114. package/dist/store/channel_inbox.js +21 -0
  115. package/dist/store/database.d.ts +10 -4
  116. package/dist/store/database.js +13 -3
  117. package/dist/store/task_journal.d.ts +32 -0
  118. package/dist/store/task_journal.js +70 -0
  119. package/dist/telegram/api.d.ts +2 -2
  120. package/dist/telegram/api.js +10 -28
  121. package/dist/telegram/gateway.d.ts +13 -27
  122. package/dist/telegram/gateway.js +86 -43
  123. package/dist/telegram/media.d.ts +2 -34
  124. package/dist/telegram/media.js +1 -180
  125. package/dist/telegram/task_preview.d.ts +13 -0
  126. package/dist/telegram/task_preview.js +92 -0
  127. package/dist/telegram/task_result.d.ts +2 -0
  128. package/dist/telegram/task_result.js +12 -0
  129. package/dist/telegram/text.d.ts +1 -1
  130. package/dist/telegram/text.js +2 -27
  131. package/dist/types.d.ts +2 -2
  132. package/docs/dsh.md +83 -0
  133. package/docs/release.md +13 -11
  134. package/docs/zh/architecture-review.md +106 -0
  135. package/docs/zh/dsh.md +83 -0
  136. package/docs/zh/release.md +13 -11
  137. package/docs/zh/telegram-experience.md +40 -0
  138. package/package.json +7 -5
package/dist/main.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { RuntimeSupervisor } from './core/runtime_supervisor.js';
2
3
  import crypto from 'node:crypto';
3
4
  import fs from 'node:fs';
4
5
  import os from 'node:os';
@@ -349,7 +350,7 @@ async function runSendVoiceCli() {
349
350
  throw new Error('Telegram voice files must be 50MB or smaller.');
350
351
  }
351
352
  const inferredBotId = parsed.botId ?? inferTelegramBotId(process.env.CODEX_HOME) ?? inferTelegramBotId(config.codexHome);
352
- const allTokens = [...config.codexBotTokens, ...config.antigravityBotTokens];
353
+ const allTokens = [...config.codexBotTokens, ...config.antigravityBotTokens, ...(config.dshBotToken ? [config.dshBotToken] : [])];
353
354
  const { botId, botToken } = resolveTelegramVoiceTarget(allTokens, inferredBotId);
354
355
  const { BridgeStore } = await import('./store/database.js');
355
356
  const store = new BridgeStore(config.storePath);
@@ -395,7 +396,7 @@ async function runSendMediaCli() {
395
396
  }
396
397
  const plan = planTelegramOutboundMedia(filePath);
397
398
  const inferredBotId = parsed.botId ?? inferTelegramBotId(process.env.CODEX_HOME) ?? inferTelegramBotId(config.codexHome);
398
- const allTokens = [...config.codexBotTokens, ...config.antigravityBotTokens];
399
+ const allTokens = [...config.codexBotTokens, ...config.antigravityBotTokens, ...(config.dshBotToken ? [config.dshBotToken] : [])];
399
400
  const { botId, botToken } = resolveTelegramVoiceTarget(allTokens, inferredBotId);
400
401
  const { BridgeStore } = await import('./store/database.js');
401
402
  const store = new BridgeStore(config.storePath);
@@ -605,7 +606,7 @@ function truncateStatusLine(value, maxLength) {
605
606
  return normalized.length <= maxLength ? normalized : `${normalized.slice(0, Math.max(0, maxLength - 1))}…`;
606
607
  }
607
608
  async function runServeCli() {
608
- const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror }, { CrossNodeAuthSync }, { OpencodeTelegramRuntime }, { UnifiedBridgeCore }, { AntigravityAppClient }, { AntigravityAuthManager, linkAntigravityAuthTokens },] = await Promise.all([
609
+ const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror }, { CrossNodeAuthSync }, { OpencodeTelegramRuntime }, { DshTelegramRuntime }, { createDshBackend }, { UnifiedBridgeCore }, { AntigravityAppClient }, { AntigravityAuthManager, linkAntigravityAuthTokens },] = await Promise.all([
609
610
  import('./channels/bridge_messaging_router.js'),
610
611
  import('./channels/telegram/telegram_messaging_port.js'),
611
612
  import('./channels/weixin/weixin_channel_adapter.js'),
@@ -621,7 +622,9 @@ async function runServeCli() {
621
622
  import('./auth/mirror.js'),
622
623
  import('./auth/cross_node_sync.js'),
623
624
  import('./opencode/runtime.js'),
624
- import('./antigravity/controller.js'),
625
+ import('./dsh/runtime.js'),
626
+ import('./dsh/backend.js'),
627
+ import('./bridge/unified_bridge.js'),
625
628
  import('./antigravity/client.js'),
626
629
  import('./antigravity/auth.js'),
627
630
  ]);
@@ -631,19 +634,81 @@ async function runServeCli() {
631
634
  const authNotificationAggregator = createAuthRefreshNotificationAggregator(logger);
632
635
  attachIlinkRuntimeFromBridgeLogger(logger, config.wxIlinkRouteTag);
633
636
  const processLock = acquireProcessLock(config.lockPath);
637
+ const supervisor = new RuntimeSupervisor();
638
+ supervisor.register('process-lock', { stop: () => processLock.release() });
634
639
  let store = null;
635
640
  let weixinAdapter = null;
636
641
  let activeWeixinCore = null;
637
- let activeTelegramAdapters = [];
638
- let managedApps = [];
642
+ const activeTelegramAdapters = [];
643
+ const managedApps = [];
639
644
  let activeAuthMirror = null;
640
645
  let activeAuthSync = null;
641
646
  let activeOpencodeRuntime = null;
647
+ let activeDshRuntime = null;
642
648
  let sharedCodexApp = null;
643
649
  let sharedAntigravityApp = null;
644
650
  let sharedAntigravityAuth = null;
645
651
  try {
646
652
  store = new BridgeStore(config.storePath);
653
+ supervisor.register('store', { stop: () => store?.close() });
654
+ supervisor.register('native-apps', { stop: async () => {
655
+ const apps = new Set([...managedApps, ...(sharedCodexApp ? [sharedCodexApp] : [])]);
656
+ const results = await Promise.allSettled([...apps].map(app => app.stop({ terminateServer: true })));
657
+ for (const result of results)
658
+ if (result.status === 'rejected')
659
+ logger.warn('codex.app-server.stop_failed', { error: serializeError(result.reason) });
660
+ } });
661
+ supervisor.register('channels-and-sync', { stop: async () => {
662
+ const failures = [];
663
+ const cleanups = [() => authNotificationAggregator.flushAll(), () => activeAuthSync?.stop(), () => activeAuthMirror?.stop(),
664
+ () => weixinAdapter?.stop(), () => activeWeixinCore?.stop({ keepTransports: true }),
665
+ ...activeTelegramAdapters.map(adapter => () => adapter.stop()),
666
+ () => activeOpencodeRuntime?.stop(), () => activeDshRuntime?.stop()];
667
+ for (const cleanup of cleanups)
668
+ try {
669
+ await cleanup();
670
+ }
671
+ catch (error) {
672
+ failures.push(error);
673
+ }
674
+ if (failures.length)
675
+ throw new AggregateError(failures, 'Runtime cleanup failed');
676
+ } });
677
+ if (config.dshBotToken) {
678
+ activeDshRuntime = new DshTelegramRuntime(config, store, logger);
679
+ await activeDshRuntime.start();
680
+ logger.info('dsh.bridge.started', activeDshRuntime.getRuntimeStatus());
681
+ }
682
+ if (config.opencodeBotToken) {
683
+ activeOpencodeRuntime = new OpencodeTelegramRuntime(config, store, logger);
684
+ await activeOpencodeRuntime.start();
685
+ logger.info('opencode.bridge.started', activeOpencodeRuntime.getRuntimeStatus());
686
+ }
687
+ if (config.dshBotToken && config.codexBotTokens.length === 0 && config.antigravityBotTokens.length === 0) {
688
+ const writeDshStatus = (running = true) => {
689
+ const status = activeDshRuntime.getRuntimeStatus();
690
+ writeRuntimeStatus(config.statusPath, {
691
+ running, connected: running && status.connected, userAgent: 'DSH ACP', botUsername: status.username,
692
+ currentBindings: store.countBindings(), pendingApprovals: status.pendingApprovals, pendingUserInputs: 0, queuedTurns: store.countQueuedTurnInputs(),
693
+ activeTurns: running ? status.activeTurns + (activeOpencodeRuntime?.getRuntimeStatus().activeTurns ?? 0) : 0, lastError: null, updatedAt: new Date().toISOString(),
694
+ channels: { telegram: running, weixin: false }, bots: [{ ...status, connected: running && status.connected }, ...(activeOpencodeRuntime ? [{ id: 'opencode', username: activeOpencodeRuntime.getRuntimeStatus().botUsername, connected: running && activeOpencodeRuntime.getRuntimeStatus().connected, activeTurns: activeOpencodeRuntime.getRuntimeStatus().activeTurns }] : [])],
695
+ });
696
+ };
697
+ writeDshStatus();
698
+ const timer = setInterval(writeDshStatus, 5000);
699
+ let shutdownPromise = null;
700
+ const shutdown = () => shutdownPromise ??= (async () => {
701
+ clearInterval(timer);
702
+ writeDshStatus(false);
703
+ const failures = await supervisor.stop();
704
+ for (const failure of failures)
705
+ logger.warn('bridge.shutdown_failed', { resource: failure.name, error: serializeError(failure.error) });
706
+ process.exit(0);
707
+ })();
708
+ process.on('SIGINT', () => void shutdown());
709
+ process.on('SIGTERM', () => void shutdown());
710
+ return;
711
+ }
647
712
  const selfUpdater = createSelfUpdateRuntime({
648
713
  entryPoint,
649
714
  nodePath: process.execPath,
@@ -658,12 +723,7 @@ async function runServeCli() {
658
723
  if (config.codexCliBin) {
659
724
  sharedCodexApp = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger, config.codexHome ? { CODEX_HOME: config.codexHome } : null, codexApiProviderOverrides);
660
725
  }
661
- if (config.opencodeBotToken) {
662
- activeOpencodeRuntime = new OpencodeTelegramRuntime(config, store, logger);
663
- await activeOpencodeRuntime.start();
664
- logger.info('opencode.bridge.started', activeOpencodeRuntime.getRuntimeStatus());
665
- }
666
- const totalBots = config.codexBotTokens.length + config.antigravityBotTokens.length;
726
+ const totalBots = config.codexBotTokens.length + config.antigravityBotTokens.length + (config.dshBotToken ? 1 : 0);
667
727
  const namespacedScopes = totalBots > 1;
668
728
  const seeds = [];
669
729
  const canonicalAuthDir = config.codexAuthDir ?? config.codexHome ?? path.join(os.homedir(), '.codex');
@@ -716,6 +776,7 @@ async function runServeCli() {
716
776
  ...codexApiProviderOverrides,
717
777
  ...(isCodexDefault ? [] : ['cli_auth_credentials_store="file"']),
718
778
  ]);
779
+ managedApps.push(codexApp);
719
780
  // Antigravity environment
720
781
  const isAntigravityDefault = backend === 'antigravity' && isDefaultRuntimeForBackend;
721
782
  let antigravityHome;
@@ -787,9 +848,8 @@ async function runServeCli() {
787
848
  restoreImportedAuthCandidateState(store, event.record.candidateName, authRuntimeIds);
788
849
  },
789
850
  });
790
- await mirror.initialize();
791
851
  activeAuthMirror = mirror;
792
- managedApps = seeds.map((runtime) => runtime.app);
852
+ await mirror.initialize();
793
853
  const lastSelfUpdatePath = path.join(APP_HOME, 'runtime', 'last-self-update.json');
794
854
  let lastSelfUpdate = readSelfUpdateStatus(lastSelfUpdatePath);
795
855
  const runtimes = [];
@@ -809,27 +869,27 @@ async function runServeCli() {
809
869
  ...(first?.codexAppServer ? { codexAppServer: first.codexAppServer } : {}),
810
870
  botUsername: first?.botUsername ?? null,
811
871
  currentBindings: store.countBindings(),
812
- pendingApprovals: store.countPendingApprovals(),
872
+ pendingApprovals: store.countPendingApprovals() + statuses.reduce((sum, status) => sum + status.pendingApprovals, 0) + (activeDshRuntime?.getRuntimeStatus().pendingApprovals ?? 0),
813
873
  pendingUserInputs: store.countPendingUserInputs(),
814
874
  queuedTurns: store.countQueuedTurnInputs(),
815
875
  activeTurns: statuses.reduce((sum, status) => sum + status.activeTurns, 0)
816
- + (weixinStatus?.activeTurns ?? 0),
876
+ + (weixinStatus?.activeTurns ?? 0) + (activeDshRuntime?.getRuntimeStatus().activeTurns ?? 0),
817
877
  lastError: statuses.find((status) => status.lastError)?.lastError
818
878
  ?? weixinStatus?.lastError
819
879
  ?? null,
820
880
  updatedAt: new Date().toISOString(),
821
881
  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
- })),
882
+ bots: [...runtimes.map((runtime, index) => ({
883
+ id: runtime.id,
884
+ username: statuses[index]?.botUsername ?? runtime.bot.username,
885
+ connected: running && Boolean(statuses[index]?.connected),
886
+ activeTurns: running ? (statuses[index]?.activeTurns ?? 0) : 0,
887
+ runtimeKind: runtime.sharedDefaultRuntime ? 'default' : 'isolated',
888
+ defaultBackend: runtime.defaultBackend,
889
+ codexHome: statuses[index]?.codexHome ?? runtime.home,
890
+ antigravityHome: runtime.antigravityHome,
891
+ ...(statuses[index]?.codexAppServer ? { codexAppServer: statuses[index].codexAppServer } : {}),
892
+ })), ...(activeDshRuntime ? [activeDshRuntime.getRuntimeStatus()] : [])],
833
893
  ...(weixinStatus ? {
834
894
  weixinRuntime: {
835
895
  connected: running && weixinStatus.connected,
@@ -844,6 +904,7 @@ async function runServeCli() {
844
904
  });
845
905
  };
846
906
  const authSyncLocalIdle = () => runtimes.every((runtime) => runtime.core.isIdleForServiceUpdate())
907
+ && (activeDshRuntime?.getRuntimeStatus().activeTurns ?? 0) === 0
847
908
  && (!activeWeixinCore || activeWeixinCore.isIdleForServiceUpdate())
848
909
  && mirror.isIdle();
849
910
  const clusterUpdateScheduler = createClusterUpdateScheduler({
@@ -896,19 +957,19 @@ async function runServeCli() {
896
957
  statusUpdated: () => writeAggregateStatus(),
897
958
  getServiceStatus: async () => ({
898
959
  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
- })),
960
+ bots: [...await Promise.all(runtimes.map(async (runtime) => {
961
+ const status = runtime.core.getRuntimeStatus();
962
+ return {
963
+ id: runtime.id,
964
+ username: status.botUsername ?? runtime.bot.username,
965
+ connected: status.connected,
966
+ activeTurns: status.activeTurns,
967
+ runtimeKind: runtime.sharedDefaultRuntime ? 'default' : 'isolated',
968
+ currentAuth: await runtime.core.getCurrentAuthLabel().catch(() => null),
969
+ codexHome: status.codexHome ?? runtime.home,
970
+ ...(status.codexAppServer ? { codexAppServer: status.codexAppServer } : {}),
971
+ };
972
+ })), ...(activeDshRuntime ? [activeDshRuntime.getRuntimeStatus()] : [])],
912
973
  ...(activeWeixinCore ? {
913
974
  weixinRuntime: {
914
975
  connected: activeWeixinCore.getRuntimeStatus().connected,
@@ -940,18 +1001,20 @@ async function runServeCli() {
940
1001
  antigravityApp: seed.antigravityApp,
941
1002
  antigravityAuth: seed.antigravityAuth,
942
1003
  defaultBackendId: seed.defaultBackend,
1004
+ backends: seed.config.dsh ? [createDshBackend(seed.config, store, logger, telegramMessaging)] : [],
943
1005
  selfUpdater,
944
1006
  });
945
- runtimes.push({ ...seed, core, telegram: new TelegramChannelAdapter(core) });
1007
+ const telegram = new TelegramChannelAdapter(core);
1008
+ activeTelegramAdapters.push(telegram);
1009
+ runtimes.push({ ...seed, core, telegram });
946
1010
  }
947
1011
  if (config.wxEnabled) {
948
1012
  const weixinApp = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger, null, codexApiProviderOverrides);
1013
+ managedApps.push(weixinApp);
949
1014
  const outbound = new BridgeMessagingRouter(new TelegramMessagingPort(seeds[0].bot), new WeixinMessagingPort(store, (id) => loadWeixinAccount(config.weixinAccountsDir, id)));
950
1015
  activeWeixinCore = new BridgeSessionCore(config, store, logger, seeds[0].bot, weixinApp, outbound, selfUpdater, coordinator, false);
951
- managedApps.push(weixinApp);
952
1016
  weixinAdapter = new WeixinChannelAdapter(activeWeixinCore, store, config, logger);
953
1017
  }
954
- activeTelegramAdapters = runtimes.map((runtime) => runtime.telegram);
955
1018
  if (config.authSyncEnabled) {
956
1019
  const authSyncTransportBot = seeds[0];
957
1020
  const authSyncTransportLabel = authSyncTransportBot.bot.username
@@ -1018,38 +1081,22 @@ async function runServeCli() {
1018
1081
  }
1019
1082
  writeAggregateStatus();
1020
1083
  logger.info('bridge.started', { bots: runtimes.map((runtime) => runtime.id) });
1021
- const shutdown = async (signal) => {
1084
+ let shutdownPromise = null;
1085
+ const shutdown = (signal) => shutdownPromise ??= (async () => {
1022
1086
  logger.info('bridge.shutting_down', { signal });
1023
- await authNotificationAggregator.flushAll();
1024
- authSync?.stop();
1025
- mirror.stop();
1026
- await weixinAdapter?.stop();
1027
- await activeWeixinCore?.stop();
1028
- await Promise.all(runtimes.map((runtime) => runtime.telegram.stop()));
1029
- await activeOpencodeRuntime?.stop();
1030
1087
  writeAggregateStatus(false);
1031
- const allManaged = [...managedApps, ...(sharedCodexApp ? [sharedCodexApp] : [])];
1032
- await Promise.all(allManaged.map((app) => app.stop({ terminateServer: true }).catch((error) => {
1033
- logger.warn('codex.app-server.stop_failed', { error: serializeError(error) });
1034
- })));
1035
- store?.close();
1036
- processLock.release();
1088
+ const failures = await supervisor.stop();
1089
+ for (const failure of failures)
1090
+ logger.warn('bridge.shutdown_failed', { resource: failure.name, error: serializeError(failure.error) });
1037
1091
  process.exit(0);
1038
- };
1092
+ })();
1039
1093
  process.on('SIGINT', () => void shutdown('SIGINT'));
1040
1094
  process.on('SIGTERM', () => void shutdown('SIGTERM'));
1041
1095
  }
1042
1096
  catch (error) {
1043
- await authNotificationAggregator.flushAll().catch(() => { });
1044
- activeAuthSync?.stop();
1045
- activeAuthMirror?.stop();
1046
- await weixinAdapter?.stop().catch(() => { });
1047
- await activeWeixinCore?.stop().catch(() => { });
1048
- await Promise.allSettled(activeTelegramAdapters.map((adapter) => adapter.stop()));
1049
- await activeOpencodeRuntime?.stop().catch(() => { });
1050
- await Promise.allSettled(managedApps.map((app) => app.stop({ terminateServer: true })));
1051
- store?.close();
1052
- processLock.release();
1097
+ const failures = await supervisor.stop();
1098
+ for (const failure of failures)
1099
+ logger.warn('bridge.shutdown_failed', { resource: failure.name, error: serializeError(failure.error) });
1053
1100
  throw error;
1054
1101
  }
1055
1102
  }
@@ -1537,10 +1584,23 @@ async function configureEnvInteractively(envPath, existed) {
1537
1584
  else {
1538
1585
  skipped.push('TG_ALLOWED_USER_ID');
1539
1586
  }
1587
+ const dshToken = sanitizeEnvInput(await rl.question('DeepSeek Harness Telegram bot token (DSH_BOT_TOKEN) [optional]: '));
1588
+ if (dshToken) {
1589
+ updates.DSH_BOT_TOKEN = dshToken;
1590
+ if (!codexTokens && !existed) {
1591
+ updates.CODEX_BOT_TOKENS = '';
1592
+ skipped.splice(skipped.indexOf('CODEX_BOT_TOKENS'), 1);
1593
+ }
1594
+ const dshSource = sanitizeEnvInput(await rl.question('DeepSeek Harness source directory (DSH_SOURCE_DIR) [Enter to use installed dsh]: '));
1595
+ if (dshSource)
1596
+ updates.DSH_SOURCE_DIR = normalizeUserPath(dshSource);
1597
+ if (!/^\d+:[A-Za-z0-9_-]+$/.test(dshToken))
1598
+ warnings.push('DSH_BOT_TOKEN does not look like a standard Telegram bot token.');
1599
+ }
1540
1600
  const cwdDefault = defaultInitCwd();
1541
1601
  const cwdPrompt = cwdDefault
1542
- ? `Default Codex workspace (DEFAULT_CWD) [${cwdDefault}]: `
1543
- : 'Default Codex workspace (DEFAULT_CWD): ';
1602
+ ? `Default workspace (DEFAULT_CWD) [${cwdDefault}]: `
1603
+ : 'Default workspace (DEFAULT_CWD): ';
1544
1604
  const cwdAnswer = sanitizeEnvInput(await rl.question(cwdPrompt));
1545
1605
  const cwd = cwdAnswer ? normalizeUserPath(cwdAnswer) : cwdDefault;
1546
1606
  if (cwd) {
@@ -1741,12 +1801,24 @@ function runDoctorChecks() {
1741
1801
  ...(process.env.ANTIGRAVITY_BOT_TOKENS ?? '').split(','),
1742
1802
  process.env.ANTIGRAVITY_BOT_TOKEN ?? '',
1743
1803
  ].map((value) => value.trim()).filter(Boolean);
1804
+ const dshToken = process.env.DSH_BOT_TOKEN?.trim();
1805
+ const dshFlag = process.env.DSH_ENABLED;
1806
+ const dshEnabled = dshFlag ? dshFlag !== 'false' && dshFlag !== '0' : Boolean(dshToken || process.env.DSH_SOURCE_DIR?.trim() || process.env.DSH_CLI_BIN?.trim());
1744
1807
  const checks = [
1745
1808
  ['node >= 24', Number(process.versions.node.split('.')[0]) >= 24],
1746
- ['codex cli available', hasConfiguredCommand(configuredCodexBin, 'codex')],
1747
- ['telegram bot token(s) configured', codexTokens.length > 0 || antigravityTokens.length > 0],
1809
+ ['telegram bot token(s) configured', codexTokens.length > 0 || antigravityTokens.length > 0 || Boolean(dshToken)],
1748
1810
  ['telegram allowed user configured', Boolean(process.env.TG_ALLOWED_USER_ID)],
1749
1811
  ];
1812
+ if (codexTokens.length > 0 || process.env.WX_ENABLED === 'true' || process.env.WX_ENABLED === '1')
1813
+ checks.push(['codex cli available', hasConfiguredCommand(configuredCodexBin, 'codex')]);
1814
+ if (dshEnabled) {
1815
+ const source = process.env.DSH_SOURCE_DIR?.trim();
1816
+ checks.push(['DSH CLI available', source ? fs.existsSync(path.join(path.resolve(source), 'apps/cli/src/bin.ts')) : hasConfiguredCommand(process.env.DSH_CLI_BIN, 'dsh')]);
1817
+ if (dshToken)
1818
+ checks.push(['DSH uses an independent Telegram bot', ![...codexTokens, ...antigravityTokens, process.env.OPENCODE_BOT_TOKEN?.trim()].includes(dshToken)]);
1819
+ }
1820
+ if (dshToken)
1821
+ checks.push(['DSH backend enabled for its Telegram bot', dshEnabled]);
1750
1822
  if (process.env.OPENCODE_BOT_TOKEN?.trim()) {
1751
1823
  const configuredOpencodeBin = process.env.OPENCODE_CLI_BIN;
1752
1824
  checks.push(['opencode cli available', hasConfiguredCommand(configuredOpencodeBin, 'opencode')]);
@@ -1,6 +1,6 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import { formatSdkError } from './client.js';
3
- import { buildAttachmentPrompt } from '../telegram/media.js';
3
+ import { buildAttachmentPrompt } from '../core/attachment_files.js';
4
4
  export class OpencodeEngineAdapter {
5
5
  client;
6
6
  defaultModel;
@@ -49,6 +49,8 @@ export class OpencodeEngineAdapter {
49
49
  }
50
50
  let finalResult = null;
51
51
  let turnError = null;
52
+ let accumulatedResponse = '';
53
+ let cleanup = () => { };
52
54
  const resultWaiters = [];
53
55
  const resolveWaiters = (res) => {
54
56
  while (resultWaiters.length > 0) {
@@ -68,8 +70,9 @@ export class OpencodeEngineAdapter {
68
70
  }
69
71
  if (cancelled)
70
72
  return;
71
- let accumulatedResponse = '';
72
73
  const onEvent = (ev) => {
74
+ if (cancelled)
75
+ return;
73
76
  if ('sessionId' in ev && ev.sessionId === sessionId) {
74
77
  if (ev.kind === 'text') {
75
78
  if (ev.delta) {
@@ -111,7 +114,7 @@ export class OpencodeEngineAdapter {
111
114
  }
112
115
  }
113
116
  };
114
- const cleanup = () => {
117
+ cleanup = () => {
115
118
  this.client.off('event', onEvent);
116
119
  };
117
120
  this.client.on('event', onEvent);
@@ -138,19 +141,35 @@ export class OpencodeEngineAdapter {
138
141
  }
139
142
  }
140
143
  catch (err) {
144
+ cleanup();
141
145
  turnError = err instanceof Error ? err : new Error(String(err));
142
146
  emitter.emit('error', turnError);
143
147
  resolveWaiters(null);
144
148
  }
145
149
  };
146
- void run();
150
+ const running = run();
151
+ let cancelling;
147
152
  return {
148
- turnId: sessionId ?? undefined,
153
+ get turnId() { return sessionId ?? undefined; },
149
154
  cancel: () => {
150
- cancelled = true;
151
- if (sessionId) {
152
- void this.client.getClient().session.abort({ sessionID: sessionId }).catch(() => { });
153
- }
155
+ if (finalResult)
156
+ return Promise.resolve();
157
+ cancelling ??= (async () => {
158
+ cancelled = true;
159
+ await running;
160
+ if (finalResult)
161
+ return;
162
+ if (sessionId) {
163
+ const result = await this.client.getClient().session.abort({ sessionID: sessionId });
164
+ if (result.error)
165
+ throw new Error(formatSdkError(result.error));
166
+ }
167
+ cleanup();
168
+ finalResult = { kind: 'result', status: 'INTERRUPTED', response: accumulatedResponse, conversationId: sessionId };
169
+ emitter.emit('result', finalResult);
170
+ resolveWaiters(finalResult);
171
+ })();
172
+ return cancelling.catch(error => { cancelling = undefined; throw error; });
154
173
  },
155
174
  waitForResult: async () => {
156
175
  if (finalResult)
@@ -91,7 +91,7 @@ export class OpencodeBridgeCore {
91
91
  async stop() {
92
92
  if (!this.started)
93
93
  return;
94
- this.bot.stop();
94
+ await this.bot.stop();
95
95
  for (const turn of this.activeTurns.values())
96
96
  this.clearTurnTimers(turn);
97
97
  for (const watch of this.watchers.values()) {
@@ -0,0 +1,14 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import type { ChannelInbound } from '../core/channel_events.js';
3
+ export interface InboxEntry {
4
+ id: string;
5
+ inbound: ChannelInbound;
6
+ }
7
+ /** Durable intake precedes business classification and acknowledgement to the transport. */
8
+ export declare class ChannelInbox {
9
+ private readonly db;
10
+ constructor(db: DatabaseSync);
11
+ accept(id: string, inbound: ChannelInbound): boolean;
12
+ complete(id: string): void;
13
+ pending(): InboxEntry[];
14
+ }
@@ -0,0 +1,21 @@
1
+ /** Durable intake precedes business classification and acknowledgement to the transport. */
2
+ export class ChannelInbox {
3
+ db;
4
+ constructor(db) {
5
+ this.db = db;
6
+ db.exec(`CREATE TABLE IF NOT EXISTS channel_inbox (
7
+ receipt_id TEXT PRIMARY KEY, scope_id TEXT NOT NULL, payload TEXT,
8
+ completed_at INTEGER, created_at INTEGER NOT NULL);
9
+ CREATE INDEX IF NOT EXISTS channel_inbox_pending ON channel_inbox(completed_at, created_at);`);
10
+ db.prepare('DELETE FROM channel_inbox WHERE completed_at < ?').run(Date.now() - 90 * 86400_000);
11
+ }
12
+ accept(id, inbound) {
13
+ this.db.prepare('INSERT OR IGNORE INTO channel_inbox VALUES (?, ?, ?, NULL, ?)').run(id, inbound.event.scopeId, JSON.stringify(inbound), Date.now());
14
+ return this.db.prepare('SELECT completed_at FROM channel_inbox WHERE receipt_id = ?').get(id)?.completed_at === null;
15
+ }
16
+ complete(id) { this.db.prepare('UPDATE channel_inbox SET payload = NULL, completed_at = ? WHERE receipt_id = ?').run(Date.now(), id); }
17
+ pending() {
18
+ return this.db.prepare('SELECT receipt_id, payload FROM channel_inbox WHERE completed_at IS NULL ORDER BY created_at, receipt_id').all()
19
+ .map(row => ({ id: String(row.receipt_id), inbound: JSON.parse(String(row.payload)) }));
20
+ }
21
+ }
@@ -1,4 +1,6 @@
1
- import type { AccessPresetValue, ActiveTurnMessageMode, AppLocale, CachedThread, ChatSessionSettings, CollaborationModeValue, GuidedPlanSessionRecord, GuidedPlanSessionState, PendingAttachmentBatchRecord, PendingAttachmentBatchStatus, PendingApprovalRecord, QueuedTurnInputRecord, QueuedTurnInputStatus, ReasoningEffortValue, ThreadBinding } from '../types.js';
1
+ import { ChannelInbox } from './channel_inbox.js';
2
+ import { TaskJournal } from './task_journal.js';
3
+ import type { AccessPresetValue, ActiveTurnMessageMode, AppLocale, CachedThread, ChatSessionSettings, CollaborationModeValue, GuidedPlanSessionRecord, GuidedPlanSessionState, PendingAttachmentBatchRecord, PendingAttachmentBatchStatus, PendingApprovalRecord, QueuedTurnInputRecord, QueuedTurnInputStatus, ThreadBinding } from '../types.js';
2
4
  import type { CumulativeTokenTotals, CumulativeTokenUsageRecord, TokenUsageDelta } from './token_usage.js';
3
5
  export interface ActiveTurnPreviewRecord {
4
6
  turnId: string;
@@ -50,6 +52,8 @@ export interface CodexAuthPoolStats {
50
52
  export type CodexAuthCandidateState = 'active' | 'needs_repair';
51
53
  export declare class BridgeStore {
52
54
  private db;
55
+ readonly taskJournal: TaskJournal;
56
+ readonly channelInbox: ChannelInbox;
53
57
  constructor(dbPath: string);
54
58
  getTelegramOffset(botKey: string): number;
55
59
  setTelegramOffset(botKey: string, updateId: number): void;
@@ -71,9 +75,11 @@ export declare class BridgeStore {
71
75
  threadId: string;
72
76
  }>;
73
77
  getChatSettings(chatId: string): ChatSessionSettings | null;
74
- setChatSettings(chatId: string, model: string | null, reasoningEffort: ReasoningEffortValue | null, locale?: AppLocale | null): void;
78
+ setChatSettings(chatId: string, model: string | null, reasoningEffort: string | null, locale?: AppLocale | null): void;
75
79
  setChatModel(chatId: string, model: string | null): void;
76
- setChatEffort(chatId: string, effort: ReasoningEffortValue | null): void;
80
+ setChatEffort(chatId: string, effort: string | null): void;
81
+ /** Persist an adapter-advertised reasoning id without imposing Codex's vocabulary. */
82
+ setChatEngineEffort(chatId: string, effort: string | null): void;
77
83
  setChatLocale(chatId: string, locale: AppLocale): void;
78
84
  setChatAccessPreset(chatId: string, accessPreset: AccessPresetValue | null): void;
79
85
  setChatCollaborationMode(chatId: string, collaborationMode: CollaborationModeValue | null): void;
@@ -140,7 +146,7 @@ export declare class BridgeStore {
140
146
  countQueuedTurnInputs(scopeId?: string): number;
141
147
  updateQueuedTurnInputStatus(queueId: string, status: QueuedTurnInputStatus, error?: string | null): void;
142
148
  cancelQueuedTurnInputs(scopeId: string): number;
143
- requeueInterruptedQueuedTurnInputs(): number;
149
+ requeueInterruptedQueuedTurnInputs(scopeId?: string): number;
144
150
  savePendingAttachmentBatch(record: PendingAttachmentBatchRecord): void;
145
151
  getPendingAttachmentBatch(batchId: string): PendingAttachmentBatchRecord | null;
146
152
  findPendingAttachmentBatchByMediaGroup(scopeId: string, mediaGroupId: string): PendingAttachmentBatchRecord | null;
@@ -1,12 +1,18 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs';
3
3
  import { DatabaseSync } from 'node:sqlite';
4
+ import { ChannelInbox } from './channel_inbox.js';
5
+ import { TaskJournal } from './task_journal.js';
4
6
  import { migrateLegacyBridgeScopeIds } from './migrate_bridge_scope.js';
5
7
  export class BridgeStore {
6
8
  db;
9
+ taskJournal;
10
+ channelInbox;
7
11
  constructor(dbPath) {
8
12
  fs.mkdirSync(path.dirname(dbPath), { recursive: true });
9
13
  this.db = new DatabaseSync(dbPath);
14
+ this.taskJournal = new TaskJournal(this.db);
15
+ this.channelInbox = new ChannelInbox(this.db);
10
16
  this.db.exec(`
11
17
  CREATE TABLE IF NOT EXISTS telegram_offsets (
12
18
  bot_key TEXT PRIMARY KEY,
@@ -403,6 +409,10 @@ export class BridgeStore {
403
409
  const current = this.getChatSettings(chatId);
404
410
  this.setChatSettings(chatId, current?.model ?? null, effort);
405
411
  }
412
+ /** Persist an adapter-advertised reasoning id without imposing Codex's vocabulary. */
413
+ setChatEngineEffort(chatId, effort) {
414
+ this.setChatEffort(chatId, effort);
415
+ }
406
416
  setChatLocale(chatId, locale) {
407
417
  const current = this.getChatSettings(chatId);
408
418
  this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, locale, current?.accessPreset ?? null, current?.collaborationMode ?? null, current?.serviceTier ?? null, current?.activeTurnMessageMode ?? null, current?.activeBackendId ?? null);
@@ -748,12 +758,12 @@ export class BridgeStore {
748
758
  `).run(Date.now(), Date.now(), scopeId);
749
759
  return Number(result.changes ?? 0);
750
760
  }
751
- requeueInterruptedQueuedTurnInputs() {
761
+ requeueInterruptedQueuedTurnInputs(scopeId) {
752
762
  const result = this.db.prepare(`
753
763
  UPDATE queued_turn_inputs
754
764
  SET status = 'queued', error = NULL, updated_at = ?, resolved_at = NULL
755
- WHERE status = 'processing'
756
- `).run(Date.now());
765
+ WHERE status = 'processing'${scopeId ? ' AND scope_id = ?' : ''}
766
+ `).run(Date.now(), ...(scopeId ? [scopeId] : []));
757
767
  return Number(result.changes ?? 0);
758
768
  }
759
769
  savePendingAttachmentBatch(record) {
@@ -0,0 +1,32 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import type { ChannelTextEvent } from '../core/channel_events.js';
3
+ import type { EngineTurnRequest, EngineTurnResult } from '../core/engine_spi.js';
4
+ export type TaskState = 'accepted' | 'queued' | 'running' | 'awaiting_confirmation' | 'delivery_pending' | 'completed' | 'failed' | 'cancelled';
5
+ export interface JournalTask {
6
+ id: string;
7
+ backendId: string;
8
+ state: TaskState;
9
+ event: ChannelTextEvent;
10
+ sourcePrompt: string;
11
+ request: EngineTurnRequest;
12
+ queueId: string | null;
13
+ previewMessageId: number;
14
+ result: EngineTurnResult | null;
15
+ delivery: string[];
16
+ deliveredChunks: number;
17
+ error: string | null;
18
+ createdAt: number;
19
+ updatedAt: number;
20
+ }
21
+ /** Original inputs, native outcome and delivery progress are distinct durable facts. */
22
+ export declare class TaskJournal {
23
+ private readonly db;
24
+ constructor(db: DatabaseSync);
25
+ atomic<T>(operation: () => T): T;
26
+ insert(task: JournalTask): void;
27
+ get(id: string): JournalTask | null;
28
+ listUnfinished(scopeId?: string): JournalTask[];
29
+ findReceipt(event: ChannelTextEvent, prompt: string): JournalTask | null;
30
+ forQueue(queueId: string): JournalTask | null;
31
+ update(id: string, state: TaskState, patch?: Partial<Omit<JournalTask, 'id' | 'backendId' | 'createdAt' | 'state'>>): JournalTask;
32
+ }