botmux 3.5.2 → 3.6.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 (458) hide show
  1. package/README.en.md +56 -396
  2. package/README.md +55 -396
  3. package/dist/.runtime-build-id +1 -0
  4. package/dist/adapters/cli/claude-code.d.ts +6 -0
  5. package/dist/adapters/cli/claude-code.d.ts.map +1 -1
  6. package/dist/adapters/cli/claude-code.js +197 -11
  7. package/dist/adapters/cli/claude-code.js.map +1 -1
  8. package/dist/adapters/cli/coco.js +4 -4
  9. package/dist/adapters/cli/coco.js.map +1 -1
  10. package/dist/adapters/cli/codex-app.d.ts.map +1 -1
  11. package/dist/adapters/cli/codex-app.js +9 -6
  12. package/dist/adapters/cli/codex-app.js.map +1 -1
  13. package/dist/adapters/cli/codex.js +5 -5
  14. package/dist/adapters/cli/codex.js.map +1 -1
  15. package/dist/adapters/cli/fs-policy.d.ts.map +1 -1
  16. package/dist/adapters/cli/fs-policy.js +5 -7
  17. package/dist/adapters/cli/fs-policy.js.map +1 -1
  18. package/dist/adapters/cli/grok.d.ts +2 -1
  19. package/dist/adapters/cli/grok.d.ts.map +1 -1
  20. package/dist/adapters/cli/grok.js +2 -1
  21. package/dist/adapters/cli/grok.js.map +1 -1
  22. package/dist/adapters/cli/hermes.d.ts.map +1 -1
  23. package/dist/adapters/cli/hermes.js +8 -4
  24. package/dist/adapters/cli/hermes.js.map +1 -1
  25. package/dist/adapters/cli/mtr.d.ts.map +1 -1
  26. package/dist/adapters/cli/mtr.js +3 -2
  27. package/dist/adapters/cli/mtr.js.map +1 -1
  28. package/dist/adapters/cli/opencode.d.ts.map +1 -1
  29. package/dist/adapters/cli/opencode.js +5 -4
  30. package/dist/adapters/cli/opencode.js.map +1 -1
  31. package/dist/adapters/cli/relay.js +2 -2
  32. package/dist/adapters/cli/relay.js.map +1 -1
  33. package/dist/adapters/cli/runner-input.d.ts +2 -2
  34. package/dist/adapters/cli/runner-input.d.ts.map +1 -1
  35. package/dist/adapters/cli/runner-input.js +9 -6
  36. package/dist/adapters/cli/runner-input.js.map +1 -1
  37. package/dist/adapters/cli/seed.d.ts.map +1 -1
  38. package/dist/adapters/cli/seed.js +2 -1
  39. package/dist/adapters/cli/seed.js.map +1 -1
  40. package/dist/adapters/cli/traex.d.ts.map +1 -1
  41. package/dist/adapters/cli/traex.js +3 -2
  42. package/dist/adapters/cli/traex.js.map +1 -1
  43. package/dist/adapters/cli/types.d.ts +25 -18
  44. package/dist/adapters/cli/types.d.ts.map +1 -1
  45. package/dist/bot-registry.d.ts +28 -0
  46. package/dist/bot-registry.d.ts.map +1 -1
  47. package/dist/bot-registry.js +75 -0
  48. package/dist/bot-registry.js.map +1 -1
  49. package/dist/cli.d.ts.map +1 -1
  50. package/dist/cli.js +272 -86
  51. package/dist/cli.js.map +1 -1
  52. package/dist/codex-app-runner.js +85 -192
  53. package/dist/codex-app-runner.js.map +1 -1
  54. package/dist/config.d.ts +0 -16
  55. package/dist/config.d.ts.map +1 -1
  56. package/dist/config.js +0 -30
  57. package/dist/config.js.map +1 -1
  58. package/dist/core/ask-broker.d.ts +12 -0
  59. package/dist/core/ask-broker.d.ts.map +1 -1
  60. package/dist/core/ask-broker.js +41 -0
  61. package/dist/core/ask-broker.js.map +1 -1
  62. package/dist/core/bot-turn-mutation-gate.d.ts +24 -0
  63. package/dist/core/bot-turn-mutation-gate.d.ts.map +1 -0
  64. package/dist/core/bot-turn-mutation-gate.js +369 -0
  65. package/dist/core/bot-turn-mutation-gate.js.map +1 -0
  66. package/dist/core/command-handler.d.ts +5 -0
  67. package/dist/core/command-handler.d.ts.map +1 -1
  68. package/dist/core/command-handler.js +38 -16
  69. package/dist/core/command-handler.js.map +1 -1
  70. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  71. package/dist/core/dashboard-ipc-server.js +330 -57
  72. package/dist/core/dashboard-ipc-server.js.map +1 -1
  73. package/dist/core/dashboard-rows.d.ts +8 -0
  74. package/dist/core/dashboard-rows.d.ts.map +1 -1
  75. package/dist/core/dashboard-rows.js.map +1 -1
  76. package/dist/core/host-overload-alert.d.ts +157 -0
  77. package/dist/core/host-overload-alert.d.ts.map +1 -0
  78. package/dist/core/host-overload-alert.js +251 -0
  79. package/dist/core/host-overload-alert.js.map +1 -0
  80. package/dist/core/passthrough-commands.d.ts +8 -0
  81. package/dist/core/passthrough-commands.d.ts.map +1 -1
  82. package/dist/core/passthrough-commands.js +19 -0
  83. package/dist/core/passthrough-commands.js.map +1 -1
  84. package/dist/core/restart-coordinator.d.ts +28 -0
  85. package/dist/core/restart-coordinator.d.ts.map +1 -0
  86. package/dist/core/restart-coordinator.js +82 -0
  87. package/dist/core/restart-coordinator.js.map +1 -0
  88. package/dist/core/restart-followup-policy.d.ts +112 -0
  89. package/dist/core/restart-followup-policy.d.ts.map +1 -0
  90. package/dist/core/restart-followup-policy.js +49 -0
  91. package/dist/core/restart-followup-policy.js.map +1 -0
  92. package/dist/core/role-library.d.ts +1 -1
  93. package/dist/core/role-library.js +1 -1
  94. package/dist/core/session-cwd.d.ts.map +1 -1
  95. package/dist/core/session-cwd.js +8 -0
  96. package/dist/core/session-cwd.js.map +1 -1
  97. package/dist/core/session-discovery.d.ts +14 -0
  98. package/dist/core/session-discovery.d.ts.map +1 -1
  99. package/dist/core/session-discovery.js +25 -0
  100. package/dist/core/session-discovery.js.map +1 -1
  101. package/dist/core/session-liveness.d.ts +15 -0
  102. package/dist/core/session-liveness.d.ts.map +1 -0
  103. package/dist/core/session-liveness.js +76 -0
  104. package/dist/core/session-liveness.js.map +1 -0
  105. package/dist/core/session-row-enrichment.d.ts +15 -0
  106. package/dist/core/session-row-enrichment.d.ts.map +1 -0
  107. package/dist/core/session-row-enrichment.js +93 -0
  108. package/dist/core/session-row-enrichment.js.map +1 -0
  109. package/dist/core/slash-inject.js +1 -1
  110. package/dist/core/slash-inject.js.map +1 -1
  111. package/dist/core/trigger-final-suppression.d.ts +24 -0
  112. package/dist/core/trigger-final-suppression.d.ts.map +1 -0
  113. package/dist/core/trigger-final-suppression.js +97 -0
  114. package/dist/core/trigger-final-suppression.js.map +1 -0
  115. package/dist/core/trigger-session.d.ts.map +1 -1
  116. package/dist/core/trigger-session.js +64 -4
  117. package/dist/core/trigger-session.js.map +1 -1
  118. package/dist/core/types.d.ts +6 -0
  119. package/dist/core/types.d.ts.map +1 -1
  120. package/dist/core/types.js.map +1 -1
  121. package/dist/core/worker-pool.d.ts +20 -0
  122. package/dist/core/worker-pool.d.ts.map +1 -1
  123. package/dist/core/worker-pool.js +283 -32
  124. package/dist/core/worker-pool.js.map +1 -1
  125. package/dist/daemon.d.ts +5 -1
  126. package/dist/daemon.d.ts.map +1 -1
  127. package/dist/daemon.js +520 -35
  128. package/dist/daemon.js.map +1 -1
  129. package/dist/dashboard/aggregator.d.ts +1 -0
  130. package/dist/dashboard/aggregator.d.ts.map +1 -1
  131. package/dist/dashboard/aggregator.js +34 -6
  132. package/dist/dashboard/aggregator.js.map +1 -1
  133. package/dist/dashboard/bot-payload.d.ts +3 -0
  134. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  135. package/dist/dashboard/bot-payload.js +3 -0
  136. package/dist/dashboard/bot-payload.js.map +1 -1
  137. package/dist/dashboard/compat.d.ts +102 -2
  138. package/dist/dashboard/compat.d.ts.map +1 -1
  139. package/dist/dashboard/compat.js +78 -14
  140. package/dist/dashboard/compat.js.map +1 -1
  141. package/dist/dashboard/connector-api.d.ts.map +1 -1
  142. package/dist/dashboard/connector-api.js +1 -0
  143. package/dist/dashboard/connector-api.js.map +1 -1
  144. package/dist/dashboard/desktop-asks.d.ts +21 -0
  145. package/dist/dashboard/desktop-asks.d.ts.map +1 -0
  146. package/dist/dashboard/desktop-asks.js +49 -0
  147. package/dist/dashboard/desktop-asks.js.map +1 -0
  148. package/dist/dashboard/public-redact.d.ts +7 -0
  149. package/dist/dashboard/public-redact.d.ts.map +1 -1
  150. package/dist/dashboard/public-redact.js +35 -3
  151. package/dist/dashboard/public-redact.js.map +1 -1
  152. package/dist/dashboard/session-presentation.d.ts +16 -0
  153. package/dist/dashboard/session-presentation.d.ts.map +1 -0
  154. package/dist/dashboard/session-presentation.js +62 -0
  155. package/dist/dashboard/session-presentation.js.map +1 -0
  156. package/dist/dashboard/settings-write-applier.d.ts +2 -1
  157. package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
  158. package/dist/dashboard/settings-write-applier.js +19 -1
  159. package/dist/dashboard/settings-write-applier.js.map +1 -1
  160. package/dist/dashboard/trigger-api.d.ts.map +1 -1
  161. package/dist/dashboard/trigger-api.js +19 -1
  162. package/dist/dashboard/trigger-api.js.map +1 -1
  163. package/dist/dashboard/vc-consumer-profiles-api.d.ts +7 -2
  164. package/dist/dashboard/vc-consumer-profiles-api.d.ts.map +1 -1
  165. package/dist/dashboard/vc-consumer-profiles-api.js +13 -0
  166. package/dist/dashboard/vc-consumer-profiles-api.js.map +1 -1
  167. package/dist/dashboard/web/app.d.ts.map +1 -1
  168. package/dist/dashboard/web/app.js +18 -5
  169. package/dist/dashboard/web/app.js.map +1 -1
  170. package/dist/dashboard/web/bot-defaults-page.d.ts +1 -1
  171. package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
  172. package/dist/dashboard/web/bot-defaults-page.js +97 -35
  173. package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
  174. package/dist/dashboard/web/bot-defaults.d.ts +3 -0
  175. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  176. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  177. package/dist/dashboard/web/client-shell.d.ts +29 -0
  178. package/dist/dashboard/web/client-shell.d.ts.map +1 -0
  179. package/dist/dashboard/web/client-shell.js +89 -0
  180. package/dist/dashboard/web/client-shell.js.map +1 -0
  181. package/dist/dashboard/web/connectors-page.d.ts.map +1 -1
  182. package/dist/dashboard/web/connectors-page.js +5 -1
  183. package/dist/dashboard/web/connectors-page.js.map +1 -1
  184. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  185. package/dist/dashboard/web/i18n.js +100 -8
  186. package/dist/dashboard/web/i18n.js.map +1 -1
  187. package/dist/dashboard/web/preferences.d.ts +7 -0
  188. package/dist/dashboard/web/preferences.d.ts.map +1 -1
  189. package/dist/dashboard/web/preferences.js +32 -0
  190. package/dist/dashboard/web/preferences.js.map +1 -1
  191. package/dist/dashboard/web/sessions-kanban.d.ts +1 -1
  192. package/dist/dashboard/web/sessions-kanban.d.ts.map +1 -1
  193. package/dist/dashboard/web/sessions-kanban.js +1 -1
  194. package/dist/dashboard/web/sessions-kanban.js.map +1 -1
  195. package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
  196. package/dist/dashboard/web/sessions-page.js +77 -23
  197. package/dist/dashboard/web/sessions-page.js.map +1 -1
  198. package/dist/dashboard/web/sessions.d.ts +4 -0
  199. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  200. package/dist/dashboard/web/sessions.js +14 -1
  201. package/dist/dashboard/web/sessions.js.map +1 -1
  202. package/dist/dashboard/web/settings-page.d.ts +6 -0
  203. package/dist/dashboard/web/settings-page.d.ts.map +1 -1
  204. package/dist/dashboard/web/settings-page.js +55 -30
  205. package/dist/dashboard/web/settings-page.js.map +1 -1
  206. package/dist/dashboard/web/store.d.ts +1 -2
  207. package/dist/dashboard/web/store.d.ts.map +1 -1
  208. package/dist/dashboard/web/store.js +69 -20
  209. package/dist/dashboard/web/store.js.map +1 -1
  210. package/dist/dashboard/web/vc-consumer-profiles-section.d.ts.map +1 -1
  211. package/dist/dashboard/web/vc-consumer-profiles-section.js +128 -42
  212. package/dist/dashboard/web/vc-consumer-profiles-section.js.map +1 -1
  213. package/dist/dashboard/webhook-routes.d.ts.map +1 -1
  214. package/dist/dashboard/webhook-routes.js +9 -2
  215. package/dist/dashboard/webhook-routes.js.map +1 -1
  216. package/dist/dashboard-web/app.js +1 -1
  217. package/dist/dashboard-web/chunks/bot-defaults-page-VQMPUJ4Y.js +10 -0
  218. package/dist/dashboard-web/chunks/chunk-AGCPF6MT.js +1 -0
  219. package/dist/dashboard-web/chunks/{chunk-KPQAUX7V.js → chunk-JMRGZQDB.js} +1 -1
  220. package/dist/dashboard-web/chunks/{chunk-R7IAKAYX.js → chunk-NP4JUBGS.js} +1 -1
  221. package/dist/dashboard-web/chunks/{chunk-NRL6CVAS.js → chunk-P327LXOT.js} +1 -1
  222. package/dist/dashboard-web/chunks/{chunk-X2L65IOM.js → chunk-SNKATZWK.js} +15 -15
  223. package/dist/dashboard-web/chunks/connectors-page-DOY4J3ZL.js +1 -0
  224. package/dist/dashboard-web/chunks/{groups-page-6DKXBYOM.js → groups-page-74TS2DIH.js} +1 -1
  225. package/dist/dashboard-web/chunks/{insights-page-NWARTJ6J.js → insights-page-AYCO65JC.js} +1 -1
  226. package/dist/dashboard-web/chunks/{monitor-room-AGX3XRJX.js → monitor-room-YKYLTSAN.js} +1 -1
  227. package/dist/dashboard-web/chunks/{monitoring-page-GGGIWN4C.js → monitoring-page-QBIUNRBR.js} +1 -1
  228. package/dist/dashboard-web/chunks/{office-page-6SIVKSK3.js → office-page-4PH2A26P.js} +1 -1
  229. package/dist/dashboard-web/chunks/overview-page-H73D5UT4.js +1 -0
  230. package/dist/dashboard-web/chunks/{roles-page-WE2YB7KN.js → roles-page-U4BTOQ7F.js} +1 -1
  231. package/dist/dashboard-web/chunks/{schedules-page-TB3I24XN.js → schedules-page-RHRHX7R7.js} +1 -1
  232. package/dist/dashboard-web/chunks/sessions-page-VSC6AM5G.js +2 -0
  233. package/dist/dashboard-web/chunks/settings-page-LF3RKM5P.js +2 -0
  234. package/dist/dashboard-web/chunks/{skills-page-3QAT2IWD.js → skills-page-SYOIGGXH.js} +1 -1
  235. package/dist/dashboard-web/chunks/{team-federation-page-K4VVAVAK.js → team-federation-page-RMBLASTW.js} +1 -1
  236. package/dist/dashboard-web/chunks/{v3-page-V6IOJR2B.js → v3-page-XJXKKJRN.js} +1 -1
  237. package/dist/dashboard-web/chunks/{whiteboards-page-V6CGKRNX.js → whiteboards-page-K3YVSTHJ.js} +1 -1
  238. package/dist/dashboard-web/style.css +551 -0
  239. package/dist/dashboard.js +153 -9
  240. package/dist/dashboard.js.map +1 -1
  241. package/dist/global-config.d.ts +11 -0
  242. package/dist/global-config.d.ts.map +1 -1
  243. package/dist/global-config.js +23 -0
  244. package/dist/global-config.js.map +1 -1
  245. package/dist/i18n/en.d.ts.map +1 -1
  246. package/dist/i18n/en.js +5 -0
  247. package/dist/i18n/en.js.map +1 -1
  248. package/dist/i18n/zh.d.ts.map +1 -1
  249. package/dist/i18n/zh.js +5 -0
  250. package/dist/i18n/zh.js.map +1 -1
  251. package/dist/im/lark/card-builder.d.ts +1 -1
  252. package/dist/im/lark/card-builder.js +1 -1
  253. package/dist/im/lark/card-handler.d.ts +10 -0
  254. package/dist/im/lark/card-handler.d.ts.map +1 -1
  255. package/dist/im/lark/card-handler.js +163 -17
  256. package/dist/im/lark/card-handler.js.map +1 -1
  257. package/dist/im/lark/client.d.ts +11 -0
  258. package/dist/im/lark/client.d.ts.map +1 -1
  259. package/dist/im/lark/client.js +47 -18
  260. package/dist/im/lark/client.js.map +1 -1
  261. package/dist/im/lark/event-dispatcher.d.ts +13 -0
  262. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  263. package/dist/im/lark/event-dispatcher.js +20 -0
  264. package/dist/im/lark/event-dispatcher.js.map +1 -1
  265. package/dist/im/lark/identity-cache.d.ts +25 -1
  266. package/dist/im/lark/identity-cache.d.ts.map +1 -1
  267. package/dist/im/lark/identity-cache.js +47 -1
  268. package/dist/im/lark/identity-cache.js.map +1 -1
  269. package/dist/im/lark/overload-nonce.d.ts +19 -0
  270. package/dist/im/lark/overload-nonce.d.ts.map +1 -0
  271. package/dist/im/lark/overload-nonce.js +72 -0
  272. package/dist/im/lark/overload-nonce.js.map +1 -0
  273. package/dist/im/lark/v3-progress-card.d.ts.map +1 -1
  274. package/dist/im/lark/v3-progress-card.js +5 -0
  275. package/dist/im/lark/v3-progress-card.js.map +1 -1
  276. package/dist/services/async-trigger-state.d.ts +82 -0
  277. package/dist/services/async-trigger-state.d.ts.map +1 -0
  278. package/dist/services/async-trigger-state.js +95 -0
  279. package/dist/services/async-trigger-state.js.map +1 -0
  280. package/dist/services/async-trigger-store.d.ts +28 -0
  281. package/dist/services/async-trigger-store.d.ts.map +1 -0
  282. package/dist/services/async-trigger-store.js +120 -0
  283. package/dist/services/async-trigger-store.js.map +1 -0
  284. package/dist/services/bot-config-store.d.ts.map +1 -1
  285. package/dist/services/bot-config-store.js +34 -2
  286. package/dist/services/bot-config-store.js.map +1 -1
  287. package/dist/services/bridge-fallback-gate.d.ts +32 -0
  288. package/dist/services/bridge-fallback-gate.d.ts.map +1 -1
  289. package/dist/services/bridge-fallback-gate.js +39 -0
  290. package/dist/services/bridge-fallback-gate.js.map +1 -1
  291. package/dist/services/bridge-turn-queue.d.ts.map +1 -1
  292. package/dist/services/bridge-turn-queue.js +15 -1
  293. package/dist/services/bridge-turn-queue.js.map +1 -1
  294. package/dist/services/card-prefs-store.d.ts +4 -0
  295. package/dist/services/card-prefs-store.d.ts.map +1 -1
  296. package/dist/services/card-prefs-store.js +8 -0
  297. package/dist/services/card-prefs-store.js.map +1 -1
  298. package/dist/services/claude-transcript.d.ts +23 -0
  299. package/dist/services/claude-transcript.d.ts.map +1 -1
  300. package/dist/services/claude-transcript.js +33 -0
  301. package/dist/services/claude-transcript.js.map +1 -1
  302. package/dist/services/codex-app-runner-protocol.d.ts +61 -0
  303. package/dist/services/codex-app-runner-protocol.d.ts.map +1 -0
  304. package/dist/services/codex-app-runner-protocol.js +170 -0
  305. package/dist/services/codex-app-runner-protocol.js.map +1 -0
  306. package/dist/services/codex-app-turn-controller.d.ts +72 -0
  307. package/dist/services/codex-app-turn-controller.d.ts.map +1 -0
  308. package/dist/services/codex-app-turn-controller.js +524 -0
  309. package/dist/services/codex-app-turn-controller.js.map +1 -0
  310. package/dist/services/codex-runner-freshness.d.ts +39 -0
  311. package/dist/services/codex-runner-freshness.d.ts.map +1 -0
  312. package/dist/services/codex-runner-freshness.js +78 -0
  313. package/dist/services/codex-runner-freshness.js.map +1 -0
  314. package/dist/services/connector-store.d.ts +6 -0
  315. package/dist/services/connector-store.d.ts.map +1 -1
  316. package/dist/services/connector-store.js.map +1 -1
  317. package/dist/services/grant-store.d.ts.map +1 -1
  318. package/dist/services/grant-store.js +16 -0
  319. package/dist/services/grant-store.js.map +1 -1
  320. package/dist/services/hermes-transcript.d.ts +1 -0
  321. package/dist/services/hermes-transcript.d.ts.map +1 -1
  322. package/dist/services/hermes-transcript.js +49 -0
  323. package/dist/services/hermes-transcript.js.map +1 -1
  324. package/dist/services/schedule-split-migration.d.ts +2 -0
  325. package/dist/services/schedule-split-migration.d.ts.map +1 -0
  326. package/dist/services/schedule-split-migration.js +159 -0
  327. package/dist/services/schedule-split-migration.js.map +1 -0
  328. package/dist/services/schedule-store.d.ts +28 -4
  329. package/dist/services/schedule-store.d.ts.map +1 -1
  330. package/dist/services/schedule-store.js +118 -33
  331. package/dist/services/schedule-store.js.map +1 -1
  332. package/dist/services/trigger-types.d.ts +18 -1
  333. package/dist/services/trigger-types.d.ts.map +1 -1
  334. package/dist/services/trigger-types.js +3 -0
  335. package/dist/services/trigger-types.js.map +1 -1
  336. package/dist/services/vc-meeting-consumer-profile-store.d.ts.map +1 -1
  337. package/dist/services/vc-meeting-consumer-profile-store.js +1 -0
  338. package/dist/services/vc-meeting-consumer-profile-store.js.map +1 -1
  339. package/dist/services/vc-meeting-consumer-profile-templates.d.ts +32 -0
  340. package/dist/services/vc-meeting-consumer-profile-templates.d.ts.map +1 -0
  341. package/dist/services/vc-meeting-consumer-profile-templates.js +106 -0
  342. package/dist/services/vc-meeting-consumer-profile-templates.js.map +1 -0
  343. package/dist/services/vc-meeting-delivery-feed.d.ts +1 -1
  344. package/dist/services/vc-meeting-delivery-feed.d.ts.map +1 -1
  345. package/dist/services/vc-meeting-delivery-feed.js +2 -1
  346. package/dist/services/vc-meeting-delivery-feed.js.map +1 -1
  347. package/dist/services/vc-meeting-delivery-hub-store.d.ts +3 -1
  348. package/dist/services/vc-meeting-delivery-hub-store.d.ts.map +1 -1
  349. package/dist/services/vc-meeting-delivery-hub-store.js +11 -0
  350. package/dist/services/vc-meeting-delivery-hub-store.js.map +1 -1
  351. package/dist/services/vc-meeting-delivery-protocol.d.ts +9 -2
  352. package/dist/services/vc-meeting-delivery-protocol.d.ts.map +1 -1
  353. package/dist/services/vc-meeting-delivery-protocol.js +10 -1
  354. package/dist/services/vc-meeting-delivery-protocol.js.map +1 -1
  355. package/dist/services/vc-meeting-delivery-receiver.d.ts.map +1 -1
  356. package/dist/services/vc-meeting-delivery-receiver.js +10 -1
  357. package/dist/services/vc-meeting-delivery-receiver.js.map +1 -1
  358. package/dist/services/vc-meeting-delivery-store.d.ts +12 -2
  359. package/dist/services/vc-meeting-delivery-store.d.ts.map +1 -1
  360. package/dist/services/vc-meeting-delivery-store.js +14 -2
  361. package/dist/services/vc-meeting-delivery-store.js.map +1 -1
  362. package/dist/services/vc-meeting-im-reply.d.ts +2 -1
  363. package/dist/services/vc-meeting-im-reply.d.ts.map +1 -1
  364. package/dist/services/vc-meeting-im-reply.js +19 -3
  365. package/dist/services/vc-meeting-im-reply.js.map +1 -1
  366. package/dist/services/vc-meeting-listener-output-protocol.d.ts +27 -0
  367. package/dist/services/vc-meeting-listener-output-protocol.d.ts.map +1 -0
  368. package/dist/services/vc-meeting-listener-output-protocol.js +58 -0
  369. package/dist/services/vc-meeting-listener-output-protocol.js.map +1 -0
  370. package/dist/services/vc-meeting-listener-topic-store.d.ts +31 -0
  371. package/dist/services/vc-meeting-listener-topic-store.d.ts.map +1 -0
  372. package/dist/services/vc-meeting-listener-topic-store.js +120 -0
  373. package/dist/services/vc-meeting-listener-topic-store.js.map +1 -0
  374. package/dist/services/vc-meeting-runtime-store.d.ts +2 -1
  375. package/dist/services/vc-meeting-runtime-store.d.ts.map +1 -1
  376. package/dist/services/vc-meeting-runtime-store.js +8 -0
  377. package/dist/services/vc-meeting-runtime-store.js.map +1 -1
  378. package/dist/services/vc-meeting-send-policy.d.ts +8 -1
  379. package/dist/services/vc-meeting-send-policy.d.ts.map +1 -1
  380. package/dist/services/vc-meeting-send-policy.js +8 -0
  381. package/dist/services/vc-meeting-send-policy.js.map +1 -1
  382. package/dist/types.d.ts +40 -3
  383. package/dist/types.d.ts.map +1 -1
  384. package/dist/utils/allowed-users-apply.d.ts +76 -0
  385. package/dist/utils/allowed-users-apply.d.ts.map +1 -0
  386. package/dist/utils/allowed-users-apply.js +123 -0
  387. package/dist/utils/allowed-users-apply.js.map +1 -0
  388. package/dist/utils/allowed-users-cache.d.ts +29 -0
  389. package/dist/utils/allowed-users-cache.d.ts.map +1 -0
  390. package/dist/utils/allowed-users-cache.js +82 -0
  391. package/dist/utils/allowed-users-cache.js.map +1 -0
  392. package/dist/utils/child-env.d.ts +2 -1
  393. package/dist/utils/child-env.d.ts.map +1 -1
  394. package/dist/utils/child-env.js +2 -1
  395. package/dist/utils/child-env.js.map +1 -1
  396. package/dist/utils/cli-usage-limit.d.ts +27 -1
  397. package/dist/utils/cli-usage-limit.d.ts.map +1 -1
  398. package/dist/utils/cli-usage-limit.js +92 -20
  399. package/dist/utils/cli-usage-limit.js.map +1 -1
  400. package/dist/utils/input-gate.d.ts +2 -0
  401. package/dist/utils/input-gate.d.ts.map +1 -1
  402. package/dist/utils/input-gate.js +2 -0
  403. package/dist/utils/input-gate.js.map +1 -1
  404. package/dist/utils/runtime-build-id.d.ts +21 -0
  405. package/dist/utils/runtime-build-id.d.ts.map +1 -0
  406. package/dist/utils/runtime-build-id.js +83 -0
  407. package/dist/utils/runtime-build-id.js.map +1 -0
  408. package/dist/utils/terminal-renderer.d.ts +3 -3
  409. package/dist/utils/terminal-renderer.js +3 -3
  410. package/dist/vc-agent/cards.d.ts.map +1 -1
  411. package/dist/vc-agent/cards.js +21 -1
  412. package/dist/vc-agent/cards.js.map +1 -1
  413. package/dist/worker.js +601 -179
  414. package/dist/worker.js.map +1 -1
  415. package/dist/workflows/hostExecutors/botmux-schedule.d.ts.map +1 -1
  416. package/dist/workflows/hostExecutors/botmux-schedule.js +13 -1
  417. package/dist/workflows/hostExecutors/botmux-schedule.js.map +1 -1
  418. package/dist/workflows/migration/v2-run-archive-schema.d.ts +20 -20
  419. package/dist/workflows/migration/v2-to-v3.d.ts.map +1 -1
  420. package/dist/workflows/migration/v2-to-v3.js +5 -1
  421. package/dist/workflows/migration/v2-to-v3.js.map +1 -1
  422. package/dist/workflows/v3/architect.d.ts.map +1 -1
  423. package/dist/workflows/v3/architect.js +1 -0
  424. package/dist/workflows/v3/architect.js.map +1 -1
  425. package/dist/workflows/v3/cli-run.d.ts +9 -0
  426. package/dist/workflows/v3/cli-run.d.ts.map +1 -1
  427. package/dist/workflows/v3/cli-run.js +17 -2
  428. package/dist/workflows/v3/cli-run.js.map +1 -1
  429. package/dist/workflows/v3/goal-cli.d.ts +58 -0
  430. package/dist/workflows/v3/goal-cli.d.ts.map +1 -0
  431. package/dist/workflows/v3/goal-cli.js +430 -0
  432. package/dist/workflows/v3/goal-cli.js.map +1 -0
  433. package/dist/workflows/v3/library-materialize.d.ts.map +1 -1
  434. package/dist/workflows/v3/library-materialize.js +5 -1
  435. package/dist/workflows/v3/library-materialize.js.map +1 -1
  436. package/dist/workflows/v3/library-schema.d.ts +21 -0
  437. package/dist/workflows/v3/library-schema.d.ts.map +1 -1
  438. package/dist/workflows/v3/library-schema.js +95 -1
  439. package/dist/workflows/v3/library-schema.js.map +1 -1
  440. package/dist/workflows/v3/progress-projection.d.ts +3 -0
  441. package/dist/workflows/v3/progress-projection.d.ts.map +1 -1
  442. package/dist/workflows/v3/progress-projection.js +23 -1
  443. package/dist/workflows/v3/progress-projection.js.map +1 -1
  444. package/dist/workflows/v3/run-envelope.d.ts +46 -1
  445. package/dist/workflows/v3/run-envelope.d.ts.map +1 -1
  446. package/dist/workflows/v3/run-envelope.js +32 -1
  447. package/dist/workflows/v3/run-envelope.js.map +1 -1
  448. package/package.json +2 -2
  449. package/dist/dashboard-web/chunks/bot-defaults-page-AUKV7P4L.js +0 -10
  450. package/dist/dashboard-web/chunks/chunk-SZOB6VK2.js +0 -1
  451. package/dist/dashboard-web/chunks/connectors-page-HDQFEXZI.js +0 -1
  452. package/dist/dashboard-web/chunks/overview-page-FVR3NKAF.js +0 -1
  453. package/dist/dashboard-web/chunks/sessions-page-KOOOASIL.js +0 -2
  454. package/dist/dashboard-web/chunks/settings-page-IEBR7TGN.js +0 -2
  455. package/dist/utils/screen-analyzer.d.ts +0 -75
  456. package/dist/utils/screen-analyzer.d.ts.map +0 -1
  457. package/dist/utils/screen-analyzer.js +0 -333
  458. package/dist/utils/screen-analyzer.js.map +0 -1
package/dist/daemon.js CHANGED
@@ -2,8 +2,9 @@ import { execFileSync } from 'node:child_process';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  import { readFileSync, existsSync, mkdirSync, unlinkSync } from 'node:fs';
4
4
  import { atomicWriteFileSync } from './utils/atomic-write.js';
5
+ import { readAllowedUsersResolveCache, writeAllowedUsersResolveCache } from './utils/allowed-users-cache.js';
5
6
  import { join, dirname } from 'node:path';
6
- import { homedir } from 'node:os';
7
+ import { homedir, loadavg, cpus, totalmem, freemem } from 'node:os';
7
8
  import { fileURLToPath } from 'node:url';
8
9
  const __filename = fileURLToPath(import.meta.url);
9
10
  const __dirname = dirname(__filename);
@@ -14,13 +15,16 @@ import { resolveBotmuxDataDir } from './core/data-dir.js';
14
15
  import { reloadExactDaemonBotConfig } from './core/daemon-config-fence.js';
15
16
  import { writeHeartbeat } from './core/daemon-heartbeat.js';
16
17
  import { botmuxWrapperFiles } from './core/botmux-wrapper.js';
18
+ import { evaluateOverload, formatOverloadAlert, buildOverloadAlertCard, buildOverloadRecoveredCard, initialOverloadCardState, DEFAULT_OVERLOAD_THRESHOLDS, INITIAL_OVERLOAD_STATE, } from './core/host-overload-alert.js';
19
+ import { registerOverloadNonce } from './im/lark/overload-nonce.js';
20
+ import { countHostOverload } from './im/lark/card-handler.js';
17
21
  import { startMaintenance, stopMaintenance } from './core/maintenance.js';
18
22
  import { selectCodexRuntimeUpdateTargets, startCliRuntimeUpdateMonitor, stopCliRuntimeUpdateMonitor, } from './core/cli-runtime-update.js';
19
23
  import { sendRestartReportIfPending } from './core/restart-report.js';
20
24
  import { statSync } from 'node:fs';
21
25
  import { addReaction, getChatMode, getChatNameAndMode, getMessageChatId, listChatMemberOpenIds, MessageWithdrawnError, replyMessage, resolveAllowedUsersWithMap, sendMessage, sendUserMessage, updateMessage } from './im/lark/client.js';
22
26
  import { resolveGroupJoinPrompt, waitForAllowedUserInChat } from './core/auto-start.js';
23
- import { loadBotConfigAtIndex, loadBotConfigs, isManagedActivationStartingAtIndex, registerBot, getBot, getAllBots, getOwnerOpenId, findOncallChat, effectiveDefaultWorkingDir, effectiveBotDisplayName, resolveVcMeetingConsumerProfiles, } from './bot-registry.js';
27
+ import { loadBotConfigAtIndex, loadBotConfigs, isManagedActivationStartingAtIndex, registerBot, getBot, getAllBots, getOwnerOpenId, findOncallChat, effectiveDefaultWorkingDir, effectiveBotDisplayName, resolveVcMeetingConsumerProfiles, setResolvedAllowedUsersRepublishHook, setAllowedUsersResolveRetryHook, } from './bot-registry.js';
24
28
  import { setDisplayNameRefresher, findConfigField, applyConfigField } from './services/bot-config-store.js';
25
29
  import { renameBotOnOpenPlatform, changeBotAvatarOnOpenPlatform } from './services/open-platform-rename.js';
26
30
  import { migrateSandboxConfigAtStartup } from './services/sandbox-migration.js';
@@ -28,6 +32,7 @@ import * as sessionStore from './services/session-store.js';
28
32
  import * as chatFirstSeenStore from './services/chat-first-seen-store.js';
29
33
  import { ensureDefaultOncallBound } from './services/oncall-store.js';
30
34
  import * as scheduleStore from './services/schedule-store.js';
35
+ import { migrateSharedSchedulesAtStartup } from './services/schedule-split-migration.js';
31
36
  import * as messageQueue from './services/message-queue.js';
32
37
  import { emitHookEvent, emitHookEventLocal, HOOK_EVENTS } from './services/hook-runner.js';
33
38
  import { setSessionLifecycleShutdown } from './services/session-lifecycle-hooks.js';
@@ -36,7 +41,8 @@ import { expandMergeForward } from './im/lark/merge-forward.js';
36
41
  import { bindResourcesToMessage, composeForwardFollowupContent, mergeMessageMentions } from './im/lark/forward-followup-content.js';
37
42
  import { buildQuoteHint } from './im/lark/quote-hint.js';
38
43
  import { logger } from './utils/logger.js';
39
- import { withFileLock } from './utils/file-lock.js';
44
+ import { applyAllowedUsersResolve } from './utils/allowed-users-apply.js';
45
+ import { withFileLock, withFileLockSync } from './utils/file-lock.js';
40
46
  import { delay } from './utils/timing.js';
41
47
  import { BoundedMap } from './utils/bounded-map.js';
42
48
  import { checkAllowedChatGroupsConfig } from './services/allowed-chat-groups.js';
@@ -116,8 +122,30 @@ let selfV3BootInstanceId;
116
122
  /** Generic daemon identity used by internal receiver endpoints. Unlike the
117
123
  * VC listener switch, every agent daemon may receive a fenced membership. */
118
124
  let selfDaemonLarkAppId;
125
+ /**
126
+ * Live dashboard descriptor for THIS daemon's single bot. Held module-level so
127
+ * the deferred allowedUsers resolve retry (a detached setTimeout that has no
128
+ * `desc` in scope) can patch `resolvedAllowedUsers` and republish coherently —
129
+ * and so the 30s heartbeat, which rewrites this same object, never clobbers a
130
+ * healed value back to stale. One daemon per bot, so a single ref is correct.
131
+ */
132
+ let selfDaemonDescriptor;
133
+ /**
134
+ * Patch the live descriptor's `resolvedAllowedUsers` (ou_ only) and rewrite it.
135
+ * Safe to call from the deferred resolve retry. Best-effort.
136
+ */
137
+ function republishResolvedAllowedUsers(larkAppId, resolved) {
138
+ const desc = selfDaemonDescriptor;
139
+ if (!desc || desc.larkAppId !== larkAppId)
140
+ return;
141
+ desc.resolvedAllowedUsers = resolved.filter(u => u.startsWith('ou_'));
142
+ try {
143
+ writeDaemonDescriptor(desc);
144
+ }
145
+ catch { /* best effort */ }
146
+ }
119
147
  let vcMeetingTerminalReconciler;
120
- import { isBotMentioned, probeBotOpenId, startLarkEventDispatcher, markForwardFollowupsSessionsReady, writeBotInfoFile, canOperate, evaluateTalk, grantCommandRestriction, isKnownPeerBot, checkRequiredScopes } from './im/lark/event-dispatcher.js';
148
+ import { isBotMentioned, probeBotOpenId, startLarkEventDispatcher, markForwardFollowupsSessionsReady, writeBotInfoFile, canOperate, canRunDaemonCommand, evaluateTalk, grantCommandRestriction, isKnownPeerBot, checkRequiredScopes } from './im/lark/event-dispatcher.js';
121
149
  import { getDocSubscription, listAllDocSubscriptions, removeDocSubscription, setDocCommentPollCursor } from './services/doc-subs-store.js';
122
150
  import { BOT_REPLY_SENTINEL, subscribeDocFile, unsubscribeDocFile, addCommentReaction, hasBotSentinel, isBotAuthoredReply, listDocComments } from './im/lark/doc-comment.js';
123
151
  import { learnFromMentions, resolveSender, flushIdentityCacheSync } from './im/lark/identity-cache.js';
@@ -143,7 +171,7 @@ import { createGroupWithBots } from './services/group-creator.js';
143
171
  import { addBotToChat, isInChat } from './services/groups-store.js';
144
172
  import { setChatReplyMode } from './services/chat-reply-mode-store.js';
145
173
  import { hasVcMeetingEndedTombstone, listVcMeetingRuntimeSessionsByListenerAndAgent, listVcMeetingRuntimeSessions, pruneExpiredVcMeetingRuntimeSessions, recordVcMeetingEndedTombstone, recordVcMeetingRuntimeSession, removeVcMeetingRuntimeSession, } from './services/vc-meeting-runtime-store.js';
146
- import { computeVcMeetingConsumerProfileHash } from './services/vc-meeting-profile-instructions.js';
174
+ import { computeVcMeetingConsumerProfileHash, normalizeVcMeetingProfileInstructions, } from './services/vc-meeting-profile-instructions.js';
147
175
  import { bootstrapVcMeetingDefaultConsumerProfile } from './services/vc-meeting-consumer-profile-bootstrap.js';
148
176
  import { getVcMeetingPreparation, normalizeVcMeetingNumber, } from './services/vc-meeting-preparations-store.js';
149
177
  import { abandonPoisonedVcMeetingDelivery, getVcMeetingDeliveryStatus, handleVcMeetingTurnTerminal, handleVcMeetingWorkerGenerationExit, receiveVcMeetingDelivery, registerVcMeetingMember, retryPoisonedVcMeetingDelivery, } from './services/vc-meeting-delivery-receiver.js';
@@ -154,6 +182,7 @@ import { applyVcMeetingHubMemberProjection, freezeVcMeetingHubDeliveryAssignment
154
182
  import { getVcMeetingFeedMetadataState, ingestVcMeetingFeedMetadata, } from './services/vc-meeting-feed-metadata-store.js';
155
183
  import { VcMeetingTerminalReconciler } from './services/vc-meeting-terminal-reconciler.js';
156
184
  import { resolveVcMeetingImTurnOrigin, verifyVcMeetingManagedOriginClaim, } from './services/vc-meeting-send-policy.js';
185
+ import { ensureVcMeetingListenerTopicRoot, } from './services/vc-meeting-listener-topic-store.js';
157
186
  import { finishVcMeetingManagedActionProvider, finishVcMeetingManagedApprovalCard, requestVcMeetingManagedImAction, requestVcMeetingManagedAction, resolveVcMeetingManagedActionApproval, } from './services/vc-meeting-action-gate.js';
158
187
  import { claimVcMeetingApprovalCardAttempt, findVcMeetingAction, listVcMeetingActionScopes, listVcMeetingActions, reconcileVcMeetingActionsOnBoot, } from './services/vc-meeting-action-store.js';
159
188
  import { authorizeVcMeetingDaemonControlRequest, ensureVcMeetingDaemonAuthToken, withVcMeetingDaemonAuthHeader, } from './services/vc-meeting-daemon-auth.js';
@@ -803,6 +832,8 @@ const VC_MEETING_SYNC_INTERVAL_OPTIONS_MS = [15_000, 30_000, 60_000, 90_000];
803
832
  const VC_MEETING_CUSTOM_SYNC_INTERVAL_MIN_SECONDS = 10;
804
833
  const VC_MEETING_CUSTOM_SYNC_INTERVAL_MAX_SECONDS = 3_600;
805
834
  const VC_MEETING_CUSTOM_SYNC_INTERVAL_FIELD = 'vc_meeting_custom_interval_seconds';
835
+ const VC_MEETING_ACTIVATION_CONTEXT_FIELD = 'vc_meeting_activation_context';
836
+ const VC_MEETING_ACTIVATION_CONTEXT_MAX_CHARS = 2_000;
806
837
  const vcMeetingEndedTombstones = new BoundedMap(2_000);
807
838
  const vcMeetingPendingInvites = new BoundedMap(2_000);
808
839
  function vcMeetingSessionKey(larkAppId, meetingId) {
@@ -899,6 +930,26 @@ function normalizeVcMeetingCustomSyncIntervalMs(value) {
899
930
  function vcMeetingCustomSyncIntervalFromCard(data) {
900
931
  return normalizeVcMeetingCustomSyncIntervalMs(data.action?.form_value?.[VC_MEETING_CUSTOM_SYNC_INTERVAL_FIELD]);
901
932
  }
933
+ function normalizeVcMeetingActivationContext(value) {
934
+ if (value === undefined || value === null)
935
+ return { ok: true };
936
+ const text = (Array.isArray(value) ? String(value[0] ?? '') : String(value))
937
+ .replace(/\r\n?/g, '\n')
938
+ .trim();
939
+ if (!text)
940
+ return { ok: true };
941
+ if (text.length > VC_MEETING_ACTIVATION_CONTEXT_MAX_CHARS) {
942
+ return { ok: false, error: `本次会议补充说明不能超过 ${VC_MEETING_ACTIVATION_CONTEXT_MAX_CHARS} 个字符` };
943
+ }
944
+ const normalized = normalizeVcMeetingProfileInstructions(text);
945
+ if (!normalized.ok) {
946
+ return { ok: false, error: `本次会议补充说明无效:${normalized.error}` };
947
+ }
948
+ return { ok: true, context: normalized.instructions };
949
+ }
950
+ function vcMeetingActivationContextFromCard(data) {
951
+ return normalizeVcMeetingActivationContext(data.action?.form_value?.[VC_MEETING_ACTIVATION_CONTEXT_FIELD]);
952
+ }
902
953
  function vcMeetingSessionFlushIntervalMs(session, cfg) {
903
954
  return session.syncIntervalMs ?? cfg.flushIntervalMs ?? DEFAULT_VC_MEETING_FLUSH_INTERVAL_MS;
904
955
  }
@@ -1889,6 +1940,20 @@ async function sessionReply(anchor, content, msgType = 'text', larkAppId, turnId
1889
1940
  return sendWithHookPolicy(chatId, content, msgType, opts.uuid);
1890
1941
  }
1891
1942
  }
1943
+ if (opts?.placement === 'chat') {
1944
+ return sendWithHookPolicy(chatId, content, msgType, opts.uuid);
1945
+ }
1946
+ if (opts?.placement === 'topic') {
1947
+ const topicKey = opts.meetingTopicKey;
1948
+ if (!topicKey || topicKey.targetChatId !== chatId) {
1949
+ throw new Error('meeting topic placement is missing a matching durable topic key');
1950
+ }
1951
+ const root = await ensureVcMeetingListenerTopicRoot(config.session.dataDir, topicKey, () => sendWithHookPolicy(chatId, content, msgType, opts.uuid));
1952
+ if (root.created) {
1953
+ return root.rootMessageId;
1954
+ }
1955
+ return replyWithHookPolicy(root.rootMessageId, content, msgType, true, opts.uuid);
1956
+ }
1892
1957
  if (ds?.scope === 'chat') {
1893
1958
  const fresh = readSessionFreshFromDisk(ds.session.sessionId, ds.larkAppId);
1894
1959
  if (fresh)
@@ -2103,6 +2168,123 @@ function removeDaemonDescriptor(larkAppId) {
2103
2168
  catch { /* ignore */ }
2104
2169
  }
2105
2170
  }
2171
+ /**
2172
+ * Persistent last-known-good `raw entry → ou_` cache for allowedUsers. The
2173
+ * read/write primitives live in utils/allowed-users-cache.ts so the runtime
2174
+ * set/revoke paths (services layer) write through the same sidecar and it never
2175
+ * diverges from the live allowlist. These thin wrappers bind the data dir.
2176
+ */
2177
+ function readAllowedUsersCache(larkAppId) {
2178
+ return readAllowedUsersResolveCache(config.session.dataDir, larkAppId);
2179
+ }
2180
+ function writeAllowedUsersCache(larkAppId, map, opts = {}) {
2181
+ writeAllowedUsersResolveCache(config.session.dataDir, larkAppId, { map, ...opts });
2182
+ }
2183
+ /** Raw entries the resolver flagged as definitively gone (safe to prune from cache). */
2184
+ function definitiveEntriesOf(entryStatus) {
2185
+ if (!entryStatus)
2186
+ return [];
2187
+ const out = [];
2188
+ for (const [entry, status] of entryStatus.entries()) {
2189
+ if (status === 'definitive')
2190
+ out.push(entry);
2191
+ }
2192
+ return out;
2193
+ }
2194
+ function notifyAllowedUsersResolveFailure(larkAppId, notice, recipients) {
2195
+ logger.error(`[${larkAppId}] ${notice}`);
2196
+ const unique = [...new Set(recipients.filter(u => typeof u === 'string' && u.startsWith('ou_')))];
2197
+ if (unique.length === 0) {
2198
+ logger.error(`[${larkAppId}] allowedUsers resolve failed with no open_id recipient available for DM; ` +
2199
+ `check daemon logs and Feishu contact API, then restart this bot.`);
2200
+ return;
2201
+ }
2202
+ // Fire-and-forget: the owner DM must NOT block daemon startup. sendUserMessage
2203
+ // uses an untimed SDK path; awaiting it on the boot-critical path (before the
2204
+ // WS listener starts) would stall the bot coming online during the very
2205
+ // contact/network partition that triggered this notice. Bound each attempt
2206
+ // with a deadline and detach.
2207
+ void (async () => {
2208
+ for (const openId of unique.slice(0, 5)) {
2209
+ try {
2210
+ await sendUserMessage(larkAppId, openId, `⚠️ Botmux allowedUsers 解析告警\n\n${notice}`, 'text', undefined, { timeoutMs: 10_000 });
2211
+ }
2212
+ catch (err) {
2213
+ logger.warn(`[${larkAppId}] failed to DM allowedUsers resolve notice to ${openId.slice(0, 12)}: ${err?.message ?? err}`);
2214
+ }
2215
+ }
2216
+ })();
2217
+ }
2218
+ function scheduleAllowedUsersResolveRetry(larkAppId, attempt = 1) {
2219
+ if (attempt > 3)
2220
+ return;
2221
+ const delayMs = attempt === 1 ? 30_000 : attempt === 2 ? 120_000 : 300_000;
2222
+ const timer = setTimeout(() => {
2223
+ void (async () => {
2224
+ let bot;
2225
+ try {
2226
+ bot = getBot(larkAppId);
2227
+ }
2228
+ catch {
2229
+ return;
2230
+ }
2231
+ const configured = bot.config.allowedUsers ?? [];
2232
+ if (configured.length === 0)
2233
+ return;
2234
+ // Fingerprint the config we resolve against. A /revoke or `set allowedUsers`
2235
+ // can land during the cross-network resolve await below; without this fence
2236
+ // the stale retry result would clobber the newer runtime/cache/descriptor —
2237
+ // reviving a just-revoked guest, or forking runtime from the persisted
2238
+ // config until the next restart. If the config changed, drop this result
2239
+ // (the mutation path already wrote through, and if it needs a retry it
2240
+ // schedules its own).
2241
+ const fingerprint = JSON.stringify(configured);
2242
+ try {
2243
+ const resolveResult = await resolveAllowedUsersWithMap(larkAppId, configured);
2244
+ let liveBot;
2245
+ try {
2246
+ liveBot = getBot(larkAppId);
2247
+ }
2248
+ catch {
2249
+ return;
2250
+ }
2251
+ if (JSON.stringify(liveBot.config.allowedUsers ?? []) !== fingerprint) {
2252
+ logger.info(`[${larkAppId}] allowedUsers resolve retry #${attempt} discarded: config changed during resolve`);
2253
+ return;
2254
+ }
2255
+ const applied = applyAllowedUsersResolve({
2256
+ rawEntries: configured,
2257
+ previousResolvedMap: readAllowedUsersCache(larkAppId),
2258
+ resolveResult,
2259
+ });
2260
+ liveBot.resolvedAllowedUsers = applied.resolved;
2261
+ liveBot.rawAllowedUserResolution = applied.map;
2262
+ // Persist the recovered map + republish the descriptor so the dashboard
2263
+ // (create-group creator picker) and the next boot both see the healed
2264
+ // open_ids — otherwise a successful retry only heals in-memory state.
2265
+ // Prune definitively-gone entries + keys no longer configured so a stale
2266
+ // owner can't be revived later.
2267
+ writeAllowedUsersCache(larkAppId, applied.map, {
2268
+ deleteEntries: definitiveEntriesOf(resolveResult.entryStatus),
2269
+ retainKeys: configured,
2270
+ });
2271
+ republishResolvedAllowedUsers(larkAppId, applied.resolved);
2272
+ if (!applied.failed) {
2273
+ logger.info(`[${larkAppId}] allowedUsers resolve retry succeeded: ${applied.resolved.join(', ') || '(empty — entries definitively gone)'}`);
2274
+ return;
2275
+ }
2276
+ logger.warn(`[${larkAppId}] allowedUsers resolve retry #${attempt} still degraded` +
2277
+ `${applied.usedFallback ? ' (on cache)' : applied.resolved.length ? '' : ' (allowlist empty)'}`);
2278
+ scheduleAllowedUsersResolveRetry(larkAppId, attempt + 1);
2279
+ }
2280
+ catch (err) {
2281
+ logger.warn(`[${larkAppId}] allowedUsers resolve retry #${attempt} threw: ${err?.message ?? err}`);
2282
+ scheduleAllowedUsersResolveRetry(larkAppId, attempt + 1);
2283
+ }
2284
+ })();
2285
+ }, delayMs);
2286
+ timer.unref?.();
2287
+ }
2106
2288
  // ─── Version tracking ────────────────────────────────────────────────────────
2107
2289
  function refreshCliVersion(cliId, cliPathOverride) {
2108
2290
  const now = Date.now();
@@ -7581,11 +7763,29 @@ function vcMeetingRuntimeAgentForProfile(profile, status, opts = {}) {
7581
7763
  ...(opts.activationError ? { activationError: opts.activationError.slice(0, 500) } : {}),
7582
7764
  ...(filter ? { filter } : {}),
7583
7765
  responseMode: profile.responseMode,
7766
+ listenerPlacement: profile.listenerDelivery?.placement ?? 'auto',
7584
7767
  capabilities: vcMeetingCanonicalProfileCapabilities(profile),
7585
7768
  ownedSinks: vcMeetingCanonicalProfileOwnedSinks(profile),
7586
7769
  deliveryProfileHash: opts.deliveryProfileHash ?? vcMeetingConsumerProfileHash(profile),
7587
7770
  };
7588
7771
  }
7772
+ function vcMeetingProfileWithActivationContext(profile, activationContext) {
7773
+ if (!activationContext)
7774
+ return profile;
7775
+ const contextSection = [
7776
+ '本次会议补充说明(仅适用于本次会议,不修改角色预设):',
7777
+ activationContext,
7778
+ ].join('\n');
7779
+ const combined = profile.instructions
7780
+ ? `${profile.instructions}\n\n${contextSection}`
7781
+ : contextSection;
7782
+ const normalized = normalizeVcMeetingProfileInstructions(combined);
7783
+ if (!normalized.ok || !normalized.instructions) {
7784
+ throw new Error(`profile ${profile.id} 的预设职责与本次会议补充说明无法合并:`
7785
+ + `${normalized.ok ? '内容为空' : normalized.error}`);
7786
+ }
7787
+ return { ...profile, instructions: normalized.instructions };
7788
+ }
7589
7789
  /** Rehydrate the exact immutable profile snapshot selected for this member
7590
7790
  * epoch. Current bots.json is only authoritative for a first join/re-add; an
7591
7791
  * edit or deletion must not rewrite an in-flight meeting after restart. */
@@ -7598,6 +7798,9 @@ function vcMeetingProfileFromRuntimeAgent(selected) {
7598
7798
  ...(selected.instructions ? { instructions: selected.instructions } : {}),
7599
7799
  ...(selected.filter ? { filter: selected.filter } : {}),
7600
7800
  responseMode: selected.responseMode,
7801
+ ...(selected.listenerPlacement !== 'auto'
7802
+ ? { listenerDelivery: { placement: selected.listenerPlacement } }
7803
+ : {}),
7601
7804
  capabilities: [...selected.capabilities],
7602
7805
  ...(selected.ownedSinks.length > 0 ? { ownedSinks: [...selected.ownedSinks] } : {}),
7603
7806
  };
@@ -7665,6 +7868,7 @@ async function ensureVcMeetingProfileMember(session, profile, listenerChatId, op
7665
7868
  && JSON.stringify(prior.ownedSinks) === JSON.stringify(canonicalOwnedSinks);
7666
7869
  const samePolicy = sameStreamSemantics
7667
7870
  && prior.responseMode === profile.responseMode
7871
+ && prior.outputPlacement === (profile.listenerDelivery?.placement ?? 'auto')
7668
7872
  && JSON.stringify(prior.capabilities) === JSON.stringify(canonicalCapabilities)
7669
7873
  && sameOwnedSinks;
7670
7874
  // Receiver registration commits before the hub projection. A crash in that
@@ -7681,6 +7885,7 @@ async function ensureVcMeetingProfileMember(session, profile, listenerChatId, op
7681
7885
  && receiverPrior.instructions === instructions
7682
7886
  && receiverPrior.outputChatId === listenerChatId
7683
7887
  && receiverPrior.responseMode === profile.responseMode
7888
+ && receiverPrior.outputPlacement === (profile.listenerDelivery?.placement ?? 'auto')
7684
7889
  && JSON.stringify(receiverPrior.filter ?? {}) === JSON.stringify(canonicalFilter ?? {})
7685
7890
  && JSON.stringify(receiverPrior.capabilities) === JSON.stringify(canonicalCapabilities)
7686
7891
  && JSON.stringify(receiverPrior.ownedSinks) === JSON.stringify(canonicalOwnedSinks)
@@ -7745,7 +7950,10 @@ async function ensureVcMeetingProfileMember(session, profile, listenerChatId, op
7745
7950
  responseMode: orphanedReceiver?.responseMode ?? profile.responseMode,
7746
7951
  ...policy,
7747
7952
  },
7748
- outputRoute: { chatId: orphanedReceiver?.outputChatId ?? listenerChatId },
7953
+ outputRoute: {
7954
+ chatId: orphanedReceiver?.outputChatId ?? listenerChatId,
7955
+ placement: orphanedReceiver?.outputPlacement ?? profile.listenerDelivery?.placement ?? 'auto',
7956
+ },
7749
7957
  };
7750
7958
  const receiver = await postVcMeetingMemberProjection(profile.agentAppId, projection);
7751
7959
  const body = vcMeetingResponseRecord(receiver.body);
@@ -7778,6 +7986,7 @@ async function ensureVcMeetingProfileMember(session, profile, listenerChatId, op
7778
7986
  membershipGeneration,
7779
7987
  status: 'active',
7780
7988
  responseMode: orphanedReceiver?.responseMode ?? profile.responseMode,
7989
+ outputPlacement: orphanedReceiver?.outputPlacement ?? profile.listenerDelivery?.placement ?? 'auto',
7781
7990
  ...policy,
7782
7991
  joinedAtIngestSeq,
7783
7992
  receiverSessionId,
@@ -7890,6 +8099,7 @@ async function pauseVcMeetingSingleConsumerMembership(session) {
7890
8099
  membershipGeneration,
7891
8100
  status: 'paused',
7892
8101
  responseMode: prior.responseMode,
8102
+ outputPlacement: prior.outputPlacement ?? 'auto',
7893
8103
  joinedAtIngestSeq: prior.joinedAtIngestSeq,
7894
8104
  receiverSessionId: prior.receiverSessionId,
7895
8105
  outputChatId: prior.outputChatId,
@@ -7915,7 +8125,7 @@ async function pauseVcMeetingSingleConsumerMembership(session) {
7915
8125
  joinedAtIngestSeq: prior.joinedAtIngestSeq,
7916
8126
  responseMode: prior.responseMode,
7917
8127
  },
7918
- outputRoute: { chatId: prior.outputChatId },
8128
+ outputRoute: { chatId: prior.outputChatId, placement: prior.outputPlacement ?? 'auto' },
7919
8129
  });
7920
8130
  const body = vcMeetingResponseRecord(receiver.body);
7921
8131
  if (receiver.status < 200 || receiver.status >= 300 || body?.ok !== true) {
@@ -7961,6 +8171,7 @@ async function pauseVcMeetingProfileMembership(session, selected) {
7961
8171
  membershipGeneration,
7962
8172
  status: 'paused',
7963
8173
  responseMode: prior.responseMode,
8174
+ outputPlacement: prior.outputPlacement ?? 'auto',
7964
8175
  ...policy,
7965
8176
  joinedAtIngestSeq: prior.joinedAtIngestSeq,
7966
8177
  receiverSessionId: prior.receiverSessionId,
@@ -7988,7 +8199,7 @@ async function pauseVcMeetingProfileMembership(session, selected) {
7988
8199
  responseMode: prior.responseMode,
7989
8200
  ...policy,
7990
8201
  },
7991
- outputRoute: { chatId: prior.outputChatId },
8202
+ outputRoute: { chatId: prior.outputChatId, placement: prior.outputPlacement ?? 'auto' },
7992
8203
  });
7993
8204
  const body = vcMeetingResponseRecord(receiver.body);
7994
8205
  if (receiver.status < 200 || receiver.status >= 300 || body?.ok !== true) {
@@ -8019,6 +8230,7 @@ function retireVcMeetingSingleConsumerForRecovery(session, reason) {
8019
8230
  membershipGeneration,
8020
8231
  status: 'removed',
8021
8232
  responseMode: member.responseMode,
8233
+ outputPlacement: member.outputPlacement ?? 'auto',
8022
8234
  joinedAtIngestSeq: member.joinedAtIngestSeq,
8023
8235
  receiverSessionId: member.receiverSessionId,
8024
8236
  outputChatId: member.outputChatId,
@@ -8038,6 +8250,7 @@ function retireVcMeetingSingleConsumerForRecovery(session, reason) {
8038
8250
  membershipGeneration,
8039
8251
  status: 'removed',
8040
8252
  responseMode: member.responseMode,
8253
+ outputPlacement: member.outputPlacement ?? 'auto',
8041
8254
  joinedAtIngestSeq: member.joinedAtIngestSeq,
8042
8255
  receiverSessionId: member.receiverSessionId,
8043
8256
  outputChatId: member.outputChatId,
@@ -8079,6 +8292,7 @@ function retireVcMeetingProfileMemberForRecovery(member, reason) {
8079
8292
  membershipGeneration,
8080
8293
  status: 'removed',
8081
8294
  responseMode: member.responseMode,
8295
+ outputPlacement: member.outputPlacement ?? 'auto',
8082
8296
  ...policy,
8083
8297
  joinedAtIngestSeq: member.joinedAtIngestSeq,
8084
8298
  receiverSessionId: member.receiverSessionId,
@@ -8100,6 +8314,7 @@ function retireVcMeetingProfileMemberForRecovery(member, reason) {
8100
8314
  membershipGeneration,
8101
8315
  status: 'removed',
8102
8316
  responseMode: member.responseMode,
8317
+ outputPlacement: member.outputPlacement ?? 'auto',
8103
8318
  ...policy,
8104
8319
  joinedAtIngestSeq: member.joinedAtIngestSeq,
8105
8320
  receiverSessionId: member.receiverSessionId,
@@ -8270,6 +8485,7 @@ function vcMeetingProfileMemberSemanticsMatchRuntime(member, selected, listenerC
8270
8485
  && member.outputChatId === listenerChatId
8271
8486
  && member.deliveryProfileHash === selected.deliveryProfileHash
8272
8487
  && member.responseMode === selected.responseMode
8488
+ && (member.outputPlacement ?? 'auto') === selected.listenerPlacement
8273
8489
  && JSON.stringify(member.filter ?? {}) === JSON.stringify(selected.filter ?? {})
8274
8490
  && JSON.stringify(member.capabilities) === JSON.stringify(selected.capabilities)
8275
8491
  && JSON.stringify(member.ownedSinks) === JSON.stringify(selected.ownedSinks);
@@ -8311,6 +8527,7 @@ function refreshVcMeetingProfileOwnerBoot(session) {
8311
8527
  joinedAtIngestSeq: member.joinedAtIngestSeq,
8312
8528
  receiverSessionId: member.receiverSessionId,
8313
8529
  outputChatId: member.outputChatId,
8530
+ outputPlacement: member.outputPlacement ?? 'auto',
8314
8531
  });
8315
8532
  if (!applied.ok)
8316
8533
  throw new Error(`receiver ${member.memberId}: ${applied.reason}`);
@@ -8336,6 +8553,7 @@ function refreshVcMeetingProfileOwnerBoot(session) {
8336
8553
  membershipGeneration: member.membershipGeneration,
8337
8554
  status: member.status,
8338
8555
  responseMode: member.responseMode,
8556
+ outputPlacement: member.outputPlacement ?? 'auto',
8339
8557
  ...policy,
8340
8558
  joinedAtIngestSeq: member.joinedAtIngestSeq,
8341
8559
  receiverSessionId: member.receiverSessionId,
@@ -9516,7 +9734,7 @@ async function removeVcMeetingProfileMember(session, selected) {
9516
9734
  ownedSinks: prior.ownedSinks,
9517
9735
  sinkOwnerGeneration: prior.sinkOwnerGeneration,
9518
9736
  },
9519
- outputRoute: { chatId: prior.outputChatId },
9737
+ outputRoute: { chatId: prior.outputChatId, placement: prior.outputPlacement ?? 'auto' },
9520
9738
  };
9521
9739
  const receiver = await postVcMeetingMemberProjection(prior.agentAppId, projection);
9522
9740
  const body = vcMeetingResponseRecord(receiver.body);
@@ -9537,6 +9755,7 @@ async function removeVcMeetingProfileMember(session, selected) {
9537
9755
  membershipGeneration,
9538
9756
  status: 'removed',
9539
9757
  responseMode: prior.responseMode,
9758
+ outputPlacement: prior.outputPlacement ?? 'auto',
9540
9759
  ...(prior.filter ? { filter: prior.filter } : {}),
9541
9760
  capabilities: prior.capabilities,
9542
9761
  ownedSinks: prior.ownedSinks,
@@ -9743,7 +9962,9 @@ async function applyVcMeetingConsumerProfileSelection(key, session, cfg, selecte
9743
9962
  const existingByProfileId = new Map(session.selectedAgents.map(selected => [selected.profileId, selected]));
9744
9963
  const requestedProfiles = resolution.selectedProfiles.map((profile) => {
9745
9964
  const existing = existingByProfileId.get(profile.id);
9746
- return existing ? vcMeetingProfileFromRuntimeAgent(existing) : profile;
9965
+ return existing
9966
+ ? vcMeetingProfileFromRuntimeAgent(existing)
9967
+ : vcMeetingProfileWithActivationContext(profile, opts.activationContext);
9747
9968
  });
9748
9969
  const retainedAfterRemovalFailure = [];
9749
9970
  const removalErrors = [];
@@ -9836,6 +10057,7 @@ async function applyVcMeetingConsumerProfileSelection(key, session, cfg, selecte
9836
10057
  session.consumerSelectionApplying = false;
9837
10058
  session.consumerPendingProfileIds = undefined;
9838
10059
  session.consumerPendingIntervalMs = undefined;
10060
+ session.consumerPendingActivationContext = undefined;
9839
10061
  }
9840
10062
  }
9841
10063
  async function activateVcMeetingPreparationBinding(session, cfg, preparation) {
@@ -9983,9 +10205,10 @@ async function applyVcMeetingConsumerProfileStagedState(key, session, cfg, fallb
9983
10205
  const selectedProfileIds = session.consumerPendingProfileIds
9984
10206
  ?? (fallbackProfileIds ? [...fallbackProfileIds] : undefined);
9985
10207
  const stagedIntervalMs = session.consumerPendingIntervalMs;
10208
+ const activationContext = session.consumerPendingActivationContext;
9986
10209
  if (stagedIntervalMs)
9987
10210
  session.syncIntervalMs = stagedIntervalMs;
9988
- let result = await applyVcMeetingConsumerProfileSelection(key, session, cfg, selectedProfileIds, opts);
10211
+ let result = await applyVcMeetingConsumerProfileSelection(key, session, cfg, selectedProfileIds, { ...opts, ...(activationContext ? { activationContext } : {}) });
9989
10212
  if (!result.ok && selectedProfileIds === undefined && session.consumerMode === 'pending') {
9990
10213
  // A conflicting/invalid hand-edited default must never leave a pending
9991
10214
  // timer retrying or partially activate a subset. Fail closed to
@@ -11566,6 +11789,11 @@ async function handleVcMeetingCardAction(data, larkAppId) {
11566
11789
  session.consumerSelectionApplying = false;
11567
11790
  }
11568
11791
  }
11792
+ const activationContext = vcMeetingActivationContextFromCard(data);
11793
+ if (!activationContext.ok) {
11794
+ return { toast: { type: 'error', content: activationContext.error } };
11795
+ }
11796
+ session.consumerPendingActivationContext = activationContext.context;
11569
11797
  return applyVcMeetingConsumerSelectionInBackground(key, session, cfg, () => applyVcMeetingConsumerProfileStagedState(key, session, cfg, undefined, { claimed: true }), { cardMessageId: data.context?.open_message_id ?? data.open_message_id });
11570
11798
  }
11571
11799
  const selection = session.consumerPendingChoice ?? { mode: 'default' };
@@ -12194,7 +12422,7 @@ async function startInitialPassthroughSession(args) {
12194
12422
  const botCfg = getBot(larkAppId).config;
12195
12423
  refreshCliVersion(botCfg.cliId, botCfg.cliPathOverride);
12196
12424
  const directChatSender = chatType === 'p2p'
12197
- ? await resolveSender(larkAppId, senderOpenId, parsed.senderType)
12425
+ ? await resolveSender(larkAppId, senderOpenId, parsed.senderType, { messageId })
12198
12426
  : undefined;
12199
12427
  // Group cold-start passthroughs only need a stable caller identity while the
12200
12428
  // repo picker is pending. Avoid adding a contact lookup to every /goal start;
@@ -12513,7 +12741,9 @@ async function handleNewTopic(data, ctx) {
12513
12741
  // chat-granted users (who only pass canTalk) management commands like
12514
12742
  // /cd /restart /oncall bind. Previously this gate only fired in oncall chats,
12515
12743
  // which left a hole once per-chat grants flow through canTalk.
12516
- if (!canOperate(larkAppId, chatId, senderOpenId, teamTrustUnionId)) {
12744
+ // canRunDaemonCommand = canOperate ∪(cmd canTalkDaemonCommands && canTalk):
12745
+ // bot 可通过名单把选定命令(如 /status)降到 canTalk;未配置时与 canOperate 全等。
12746
+ if (!canRunDaemonCommand(larkAppId, chatId, senderOpenId, teamTrustUnionId, cmd, senderUnionId, chatType)) {
12517
12747
  await sessionReply(anchor, tr('daemon.cmd_allowed_users_only', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
12518
12748
  return;
12519
12749
  }
@@ -12547,7 +12777,7 @@ async function handleNewTopic(data, ctx) {
12547
12777
  const session = sessionStore.createSession(chatId, cmdRootIdForStore, cmdContent.substring(0, 50), chatType);
12548
12778
  const now = Date.now();
12549
12779
  if (chatType === 'p2p') {
12550
- setDirectChatDisplayNameFromSender(session, chatType, await resolveSender(larkAppId, senderOpenId, parsed.senderType));
12780
+ setDirectChatDisplayNameFromSender(session, chatType, await resolveSender(larkAppId, senderOpenId, parsed.senderType, { messageId }));
12551
12781
  }
12552
12782
  session.larkAppId = larkAppId;
12553
12783
  session.ownerOpenId = senderOpenId;
@@ -12622,7 +12852,7 @@ async function handleNewTopic(data, ctx) {
12622
12852
  // resolveSender for an unseen open_id may await contact.v3.user.get with a
12623
12853
  // short budget; subsequent calls hit the cache and are sync-fast.
12624
12854
  const groupChatNamePromise = resolveGroupChatNameForNativeTitle(larkAppId, chatId, chatType, botCfg.cliId, parsed.content, parsed.mentions);
12625
- const newTopicSender = await resolveSender(larkAppId, senderOpenId, parsed.senderType);
12855
+ const newTopicSender = await resolveSender(larkAppId, senderOpenId, parsed.senderType, { messageId });
12626
12856
  refreshCliVersion(botCfg.cliId, botCfg.cliPathOverride);
12627
12857
  // Pin the working dir via the layered oncall / inherit / default lookup
12628
12858
  // (auto-binds a defaultOncall chat as a side effect). Shared with the
@@ -13084,7 +13314,9 @@ async function handleThreadReply(data, ctx) {
13084
13314
  if (threadSenderResolved)
13085
13315
  return threadSenderCached;
13086
13316
  threadSenderResolved = true;
13087
- threadSenderCached = await resolveSender(larkAppId, senderOpenIdForPrefix, parsed.senderType, isForeignBot ? { type: 'bot', name: foreignBotName !== 'Bot' ? foreignBotName : undefined } : undefined);
13317
+ threadSenderCached = await resolveSender(larkAppId, senderOpenIdForPrefix, parsed.senderType, isForeignBot
13318
+ ? { type: 'bot', name: foreignBotName !== 'Bot' ? foreignBotName : undefined, messageId: parsed.messageId }
13319
+ : { messageId: parsed.messageId });
13088
13320
  return threadSenderCached;
13089
13321
  };
13090
13322
  const content = parsed.content.trim();
@@ -13286,7 +13518,9 @@ async function handleThreadReply(data, ctx) {
13286
13518
  }
13287
13519
  // canOperate gate for thread-reply daemon commands — required in every chat
13288
13520
  // (see spawn-path gate above). Denies chat-granted users management commands.
13289
- if (!canOperate(larkAppId, effectiveThreadChatId, threadSenderOpenId, threadTeamTrustUnionId)) {
13521
+ // canRunDaemonCommand:canTalkDaemonCommands 名单内的命令降到 canTalk,
13522
+ // 与 new-topic 路径的统一闸同款(未配置时与 canOperate 全等)。
13523
+ if (!canRunDaemonCommand(larkAppId, effectiveThreadChatId, threadSenderOpenId, threadTeamTrustUnionId, cmd, threadSenderUnionId, ctxChatType)) {
13290
13524
  sessionReply(anchor, tr('daemon.cmd_allowed_users_only', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
13291
13525
  return;
13292
13526
  }
@@ -14199,6 +14433,111 @@ function resolvePrimaryOwnerOpenId(larkAppId) {
14199
14433
  return undefined;
14200
14434
  }
14201
14435
  }
14436
+ /** Parse a positive float env override, falling back to `dflt` when unset/invalid. */
14437
+ function envFloat(name, dflt) {
14438
+ const raw = process.env[name];
14439
+ if (raw === undefined || raw.trim() === '')
14440
+ return dflt;
14441
+ const n = Number(raw);
14442
+ return Number.isFinite(n) && n >= 0 ? n : dflt;
14443
+ }
14444
+ /**
14445
+ * Host-overload thresholds, seeded from module defaults and overridable via env
14446
+ * so an operator can tune sensitivity without a code change:
14447
+ * BOTMUX_OVERLOAD_ENTER_LOAD_RATIO / _EXIT_LOAD_RATIO (× logical CPU count)
14448
+ * BOTMUX_OVERLOAD_ENTER_MEM_FRAC / _EXIT_MEM_FRAC (0..1)
14449
+ * BOTMUX_OVERLOAD_MIN_REALERT_MS
14450
+ * Setting BOTMUX_OVERLOAD_ALERT=0 disables the watcher entirely (checked at the
14451
+ * call site). Swap is not read on this platform, so its thresholds stay default
14452
+ * but are inert (reading passes swap=undefined).
14453
+ */
14454
+ function resolveOverloadThresholds() {
14455
+ const cpuCount = Math.max(1, cpus().length || 1);
14456
+ const enterLoadRatio = envFloat('BOTMUX_OVERLOAD_ENTER_LOAD_RATIO', DEFAULT_OVERLOAD_THRESHOLDS.enterLoadRatio);
14457
+ let exitLoadRatio = envFloat('BOTMUX_OVERLOAD_EXIT_LOAD_RATIO', DEFAULT_OVERLOAD_THRESHOLDS.exitLoadRatio);
14458
+ const enterMemUsedFrac = envFloat('BOTMUX_OVERLOAD_ENTER_MEM_FRAC', DEFAULT_OVERLOAD_THRESHOLDS.enterMemUsedFrac);
14459
+ let exitMemUsedFrac = envFloat('BOTMUX_OVERLOAD_EXIT_MEM_FRAC', DEFAULT_OVERLOAD_THRESHOLDS.exitMemUsedFrac);
14460
+ // Hysteresis only works when the recover line sits STRICTLY below the enter
14461
+ // line. Clamping a misconfigured exit down to *equal* enter is not enough: at
14462
+ // enter == exit the enter test (mem `>=`) and the recover test (mem `<=`) both
14463
+ // fire at the exact threshold, so a reading pinned there flaps entered/
14464
+ // recovered every tick. Force a small gap below enter (5%) and warn.
14465
+ const HYSTERESIS_MARGIN = 0.95; // exit = 95% of enter when misconfigured
14466
+ if (exitLoadRatio >= enterLoadRatio) {
14467
+ const clamped = enterLoadRatio * HYSTERESIS_MARGIN;
14468
+ logger.warn(`[overload] BOTMUX_OVERLOAD_EXIT_LOAD_RATIO (${exitLoadRatio}) >= ENTER (${enterLoadRatio}); clamping exit to ${clamped}`);
14469
+ exitLoadRatio = clamped;
14470
+ }
14471
+ if (exitMemUsedFrac >= enterMemUsedFrac) {
14472
+ const clamped = enterMemUsedFrac * HYSTERESIS_MARGIN;
14473
+ logger.warn(`[overload] BOTMUX_OVERLOAD_EXIT_MEM_FRAC (${exitMemUsedFrac}) >= ENTER (${enterMemUsedFrac}); clamping exit to ${clamped}`);
14474
+ exitMemUsedFrac = clamped;
14475
+ }
14476
+ return {
14477
+ cpuCount,
14478
+ enterLoadRatio,
14479
+ exitLoadRatio,
14480
+ enterMemUsedFrac,
14481
+ exitMemUsedFrac,
14482
+ enterSwapUsedFrac: DEFAULT_OVERLOAD_THRESHOLDS.enterSwapUsedFrac,
14483
+ exitSwapUsedFrac: DEFAULT_OVERLOAD_THRESHOLDS.exitSwapUsedFrac,
14484
+ minReAlertMs: envFloat('BOTMUX_OVERLOAD_MIN_REALERT_MS', DEFAULT_OVERLOAD_THRESHOLDS.minReAlertMs),
14485
+ };
14486
+ }
14487
+ /**
14488
+ * Machine-wide de-dup for the host-overload alert. load/mem are host-wide, so if
14489
+ * more than one bot has the `overloadAlert` toggle on, every such daemon would
14490
+ * independently detect the same edge and DM its owner. This claims a short-lived
14491
+ * episode marker in the shared botmux data dir so only the first daemon to see a
14492
+ * given edge actually sends; siblings within the dedup window back off.
14493
+ *
14494
+ * The key is just the edge kind (`entered` / `recovered`): within DEDUP_WINDOW_MS
14495
+ * the first daemon to see an edge claims it and siblings back off. (An earlier
14496
+ * version bucketed `entered` by `Math.round(load15)`, but sibling daemons
14497
+ * sampling load15 either side of an X.5 boundary rounded to different bands,
14498
+ * got different keys, and each DMed — so the band is dropped.)
14499
+ *
14500
+ * The claim is a real mutual exclusion: the ENTIRE read-check-write runs inside
14501
+ * one `withFileLockSync` critical section on the marker. An earlier version used
14502
+ * `openSync(marker, 'wx')`, but that only serializes the FIRST claim — once the
14503
+ * marker exists (it persists after the first alert), every later edge
14504
+ * (recovered / expired) fell back to an unlocked read→replace, so two daemons
14505
+ * both detecting the same `recovered` edge could both read the stale key, both
14506
+ * replace it, and each DM. Serializing the whole section closes that hole. Best-
14507
+ * effort: if the lock itself can't be taken (FS error), allow the DM (better a
14508
+ * rare duplicate than a silent miss).
14509
+ */
14510
+ function claimOverloadEpisode(kind) {
14511
+ const DEDUP_WINDOW_MS = 60_000; // ≥ two 30s ticks; covers sibling daemons racing the same edge.
14512
+ const marker = join(config.session.dataDir, '.overload-episode.json');
14513
+ const key = kind;
14514
+ try {
14515
+ return withFileLockSync(marker, () => {
14516
+ const now = Date.now();
14517
+ if (existsSync(marker)) {
14518
+ try {
14519
+ const prev = JSON.parse(readFileSync(marker, 'utf8'));
14520
+ if (prev.key === key && typeof prev.at === 'number' && now - prev.at < DEDUP_WINDOW_MS) {
14521
+ return false; // Same edge already claimed within the window.
14522
+ }
14523
+ }
14524
+ catch {
14525
+ // Corrupt marker → fall through and take it over.
14526
+ }
14527
+ }
14528
+ // Winner: claim the episode. atomicWriteFileSync is fine here — the lock,
14529
+ // not the write, provides mutual exclusion.
14530
+ atomicWriteFileSync(marker, JSON.stringify({ key, at: now }));
14531
+ return true;
14532
+ }, { maxWaitMs: 2_000 });
14533
+ }
14534
+ catch (err) {
14535
+ // Couldn't acquire the lock (timeout / FS error) → don't silently drop the
14536
+ // edge; allow this DM (a rare duplicate is better than a missed alert).
14537
+ logger.warn(`[overload] episode claim lock failed (${kind}): ${err instanceof Error ? err.message : String(err)}; allowing DM`);
14538
+ return true;
14539
+ }
14540
+ }
14202
14541
  /** Build the current dashboard URL (active token, not a rotation) from the
14203
14542
  * dashboard process's persisted `.dashboard-port` / `.dashboard-token`. Falls
14204
14543
  * back to a token-less base URL if the dashboard hasn't published a token yet. */
@@ -14399,8 +14738,12 @@ export async function startDaemon(botIndex) {
14399
14738
  }
14400
14739
  }, VC_MEETING_DELIVERY_LEASE_SCAN_MS);
14401
14740
  vcMeetingDeliveryLeaseTimer.unref?.();
14402
- // Watch schedules.json for external writes (e.g. `botmux schedule add`
14741
+ // Bind the schedule store to this daemon's bot (per-bot stores live in each
14742
+ // BOT_HOME), split a legacy shared data/schedules.json if one still exists,
14743
+ // then watch our own store for external writes (e.g. `botmux schedule add`
14403
14744
  // running in a separate node process) so dashboard event bus stays in sync.
14745
+ scheduleStore.setScheduleScope(cfg.larkAppId);
14746
+ migrateSharedSchedulesAtStartup(botConfigs.map(b => b.larkAppId), botConfigs[0]?.larkAppId ?? cfg.larkAppId);
14404
14747
  scheduleStore.startExternalWriteWatcher();
14405
14748
  logger.info(`Bot ${idx}/${botConfigs.length}: ${cfg.larkAppId} (cli: ${cfg.cliId})`);
14406
14749
  setAskCardDispatcher(createLarkAskCardDispatcher());
@@ -14434,6 +14777,16 @@ export async function startDaemon(botIndex) {
14434
14777
  // briefly sees an unusable on_/email (the resolution below rewrites this field).
14435
14778
  resolvedAllowedUsers: getBot(cfg.larkAppId).resolvedAllowedUsers.filter(u => u.startsWith('ou_')),
14436
14779
  };
14780
+ // Expose the live descriptor module-level so the deferred allowedUsers resolve
14781
+ // retry can republish healed open_ids coherently (see republishResolvedAllowedUsers).
14782
+ selfDaemonDescriptor = desc;
14783
+ // Let runtime allowedUsers mutations (set / revoke, in the services layer)
14784
+ // republish the descriptor through the same path without importing daemon
14785
+ // internals. Registered once per daemon; one daemon per bot.
14786
+ setResolvedAllowedUsersRepublishHook(republishResolvedAllowedUsers);
14787
+ // And let a mutation that hit a transient contact failure schedule the same
14788
+ // background resolve-retry the startup path uses (heal-when-API-recovers).
14789
+ setAllowedUsersResolveRetryHook((appId) => scheduleAllowedUsersResolveRetry(appId));
14437
14790
  // 名称状态刷新:displayName 或飞书真名变化后,用有效展示名刷新 descriptor +
14438
14791
  // SessionRow.botName,无需重启 daemon。displayName 路径经 bot-config-store 的
14439
14792
  // 钩子触发;真·改名路径在下面的 renamer 里直接调用。
@@ -14683,22 +15036,68 @@ export async function startDaemon(botIndex) {
14683
15036
  // Refresh CLI version per bot's cliId
14684
15037
  refreshCliVersion(cfg.cliId, cfg.cliPathOverride);
14685
15038
  // Resolve allowed users per bot
14686
- if (bot.resolvedAllowedUsers.length > 0) {
15039
+ if ((bot.config.allowedUsers?.length ?? 0) > 0 || bot.resolvedAllowedUsers.length > 0) {
14687
15040
  // 含邮箱或 union_id(on_) 都要重解析成本 app 的 open_id —— 否则 canTalk/canOperate
14688
15041
  // 拿 sender 的 ou_ 对不上 on_,owner 会被自己的 bot 锁死(PR#72)。
14689
15042
  // literal ou_ 也走 best-effort 校验,用于诊断把其他 app 视角 open_id
14690
15043
  // 误填到本 bot 的配置。
14691
- const needsResolve = bot.resolvedAllowedUsers.some(u => u.includes('@') || u.startsWith('on_') || u.startsWith('ou_'));
15044
+ //
15045
+ // Transient contact failures must NOT blank the runtime list: an empty
15046
+ // resolved allowlist under a configured allowedUsers is fail-closed and
15047
+ // silently drops even the real owner (@ with no group reply). Reuse the
15048
+ // last-known-good `raw → ou_` cache PER ENTRY (only for still-configured,
15049
+ // transient-failed entries), log loudly, DM owners (fire-and-forget), and
15050
+ // schedule retries. The cache is a persistent sidecar — NOT the dashboard
15051
+ // descriptor, which is overwritten early in boot and deleted on shutdown.
15052
+ const configured = bot.config.allowedUsers ?? bot.resolvedAllowedUsers;
15053
+ const needsResolve = configured.some(u => u.includes('@') || u.startsWith('on_') || u.startsWith('ou_'));
14692
15054
  if (needsResolve) {
15055
+ const previousResolvedMap = readAllowedUsersCache(cfg.larkAppId);
14693
15056
  try {
14694
- // 同时拿到 raw→open_id 映射,供 /revoke 反查删除 email 形式的 raw 条目(R2#2)。
14695
- const { resolved, map } = await resolveAllowedUsersWithMap(cfg.larkAppId, bot.resolvedAllowedUsers);
14696
- bot.resolvedAllowedUsers = resolved;
14697
- bot.rawAllowedUserResolution = map;
14698
- logger.info(`[${cfg.larkAppId}] Resolved allowedUsers: ${bot.resolvedAllowedUsers.join(', ')}`);
15057
+ // 同时拿到 raw→open_id 映射,供 /revoke 反查删除 email 形式的 raw 条目(R2#2)。
15058
+ const resolveResult = await resolveAllowedUsersWithMap(cfg.larkAppId, configured);
15059
+ const applied = applyAllowedUsersResolve({
15060
+ rawEntries: configured,
15061
+ previousResolvedMap,
15062
+ resolveResult,
15063
+ });
15064
+ bot.resolvedAllowedUsers = applied.resolved;
15065
+ bot.rawAllowedUserResolution = applied.map;
15066
+ // Persist the fresh/recovered map so the next boot (and a clean
15067
+ // restart, which deletes the descriptor) can fall back per-entry.
15068
+ // Prune definitively-gone entries + keys no longer configured.
15069
+ writeAllowedUsersCache(cfg.larkAppId, applied.map, {
15070
+ deleteEntries: definitiveEntriesOf(resolveResult.entryStatus),
15071
+ retainKeys: configured,
15072
+ });
15073
+ logger.info(`[${cfg.larkAppId}] Resolved allowedUsers: ${bot.resolvedAllowedUsers.join(', ') || '(empty)'}${applied.usedFallback ? ' [some from cache]' : ''}`);
15074
+ if (applied.failed && applied.notice) {
15075
+ notifyAllowedUsersResolveFailure(cfg.larkAppId, applied.notice, applied.resolved);
15076
+ scheduleAllowedUsersResolveRetry(cfg.larkAppId);
15077
+ }
14699
15078
  }
14700
15079
  catch (err) {
14701
- logger.warn(`[${cfg.larkAppId}] Failed to resolve allowedUsers: ${err.message}`);
15080
+ // A full throw is a transient outage: mark every contact-resolvable
15081
+ // entry transient so the pure fn can recover each from cache.
15082
+ const throwStatus = new Map();
15083
+ for (const e of configured) {
15084
+ if (e.includes('@') || e.startsWith('on_') || e.startsWith('ou_'))
15085
+ throwStatus.set(e, 'transient');
15086
+ }
15087
+ const applied = applyAllowedUsersResolve({
15088
+ rawEntries: configured,
15089
+ previousResolvedMap,
15090
+ resolveResult: { resolved: [], map: new Map(), errored: true, entryStatus: throwStatus },
15091
+ });
15092
+ bot.resolvedAllowedUsers = applied.resolved;
15093
+ bot.rawAllowedUserResolution = applied.map;
15094
+ if (applied.usedFallback) {
15095
+ writeAllowedUsersCache(cfg.larkAppId, applied.map, { retainKeys: configured });
15096
+ }
15097
+ const notice = applied.notice
15098
+ ?? `Failed to resolve allowedUsers: ${err?.message ?? err}`;
15099
+ notifyAllowedUsersResolveFailure(cfg.larkAppId, `${notice} (throw: ${err?.message ?? err})`, applied.resolved);
15100
+ scheduleAllowedUsersResolveRetry(cfg.larkAppId);
14702
15101
  }
14703
15102
  }
14704
15103
  // Republish the descriptor with the post-resolution open_ids so the
@@ -14898,10 +15297,10 @@ export async function startDaemon(botIndex) {
14898
15297
  }, 5_000);
14899
15298
  docCommentPollTimer.unref?.();
14900
15299
  void pollWatchedDocComments(cfg.larkAppId);
14901
- // Sweep orphan sandbox overlays left by a previous run's crash/kill: any
15300
+ // Sweep orphan sandbox trees left by a previous run's crash/kill: any
14902
15301
  // <dataDir>/sandboxes/<sid> whose session is no longer active gets its
14903
- // overlays unmounted and its dirs removed (plus the /var/tmp home scratch).
14904
- // Active sessions keep theirs — a same-topic worker reuses the upper changeset.
15302
+ // deny-mask mountpoints reclaimed and its dirs removed.
15303
+ // Active sessions keep theirs — a same-topic worker reuses the tree.
14905
15304
  try {
14906
15305
  sweepOrphanSandboxes(config.session.dataDir, new Set([...activeSessions.values()].map(ds => ds.session.sessionId)));
14907
15306
  }
@@ -14915,12 +15314,12 @@ export async function startDaemon(botIndex) {
14915
15314
  }, 60_000);
14916
15315
  idleWorkerSweepTimer.unref?.();
14917
15316
  // Periodic sandbox reconciler: the daemon's SIGKILL straggler-reaper (and any
14918
- // worker SIGKILL) bypasses worker-side killCli(), so the overlay mounts +
14919
- // upper/work dirs of a killed-but-still-active sandboxed session would leak for
14920
- // the rest of this daemon's lifetime (one daemon per bot can run for days). The
14921
- // startup sweep alone can't catch a session that dies AFTER boot. This re-runs
14922
- // the sweep on a timer: it reclaims active sessions whose overlays are already
14923
- // unmounted (= worker/CLI dead) without ever tearing down a live mount.
15317
+ // worker SIGKILL) bypasses worker-side killCli(), so the pre-created deny-mask
15318
+ // mountpoints + per-session tree of a killed-but-still-active sandboxed session
15319
+ // would leak for the rest of this daemon's lifetime (one daemon per bot can run
15320
+ // for days). The startup sweep alone can't catch a session that dies AFTER boot.
15321
+ // This re-runs the sweep on a timer: it reclaims sessions whose worker/CLI is
15322
+ // dead without ever tearing down a live one.
14924
15323
  const sandboxReconcileTimer = setInterval(() => {
14925
15324
  try {
14926
15325
  sweepOrphanSandboxes(config.session.dataDir, new Set([...activeSessions.values()].map(ds => ds.session.sessionId)));
@@ -14990,6 +15389,92 @@ export async function startDaemon(botIndex) {
14990
15389
  });
14991
15390
  }, 5_000).unref?.();
14992
15391
  }
15392
+ // Host-overload watcher. Enabled per-bot via the `overloadAlert` config toggle
15393
+ // (dashboard → Groups & Bots → bot card), NOT hardcoded to the primary daemon:
15394
+ // any one bot can be designated the machine's alerter. load/mem are host-wide,
15395
+ // so if more than one bot has it on, a cross-process file lock keyed on the
15396
+ // overload episode ensures the machine only DMs once per edge (whichever bot's
15397
+ // daemon wins the lock speaks). Reads os.loadavg()[2] + mem every 30s; on a
15398
+ // healthy→overloaded or overloaded→healthy edge it DMs that bot's owner.
15399
+ // Hysteresis + a min re-alert window (see host-overload-alert.ts) keep a load
15400
+ // hovering near the line from spamming. Set BOTMUX_OVERLOAD_ALERT=0 to force
15401
+ // the whole feature off regardless of per-bot config.
15402
+ if (process.env.BOTMUX_OVERLOAD_ALERT !== '0') {
15403
+ const overloadThresholds = resolveOverloadThresholds();
15404
+ let overloadState = INITIAL_OVERLOAD_STATE;
15405
+ const overloadTimer = setInterval(() => {
15406
+ void (async () => {
15407
+ try {
15408
+ // Per-bot热开关:关了就不采样,但 timer 常驻,用户在 web 打开后下个 tick 即生效。
15409
+ let enabled = false;
15410
+ try {
15411
+ enabled = getBot(cfg.larkAppId).config.overloadAlert === true;
15412
+ }
15413
+ catch {
15414
+ enabled = false;
15415
+ }
15416
+ if (!enabled) {
15417
+ overloadState = INITIAL_OVERLOAD_STATE;
15418
+ return;
15419
+ }
15420
+ const reading = {
15421
+ load15: loadavg()[2] ?? 0,
15422
+ memTotalBytes: totalmem(),
15423
+ memFreeBytes: freemem(),
15424
+ };
15425
+ const { nextState, action } = evaluateOverload(overloadState, reading, overloadThresholds, Date.now());
15426
+ overloadState = nextState;
15427
+ if (!action)
15428
+ return;
15429
+ // Machine-wide de-dup: multiple bots may have the toggle on, but the
15430
+ // host is one machine. Claim a short-lived episode lock so only one bot
15431
+ // DMs per edge. Losing the claim = another bot already alerted this edge.
15432
+ if (!claimOverloadEpisode(action.kind)) {
15433
+ logger.info(`[overload] ${action.kind} edge already claimed by a sibling bot; skipping DM (${cfg.larkAppId})`);
15434
+ return;
15435
+ }
15436
+ const ownerOpenId = resolvePrimaryOwnerOpenId(cfg.larkAppId);
15437
+ logger.info(`[overload] ${action.kind}: load15=${action.metrics.load15.toFixed(2)} `
15438
+ + `perCpu=${action.metrics.loadPerCpu.toFixed(2)} mem=${(action.metrics.memUsedFrac * 100).toFixed(0)}% `
15439
+ + `reasons=[${action.reasons.join(',')}] owner=${ownerOpenId ? 'yes' : 'none'} bot=${cfg.larkAppId}`);
15440
+ if (!ownerOpenId)
15441
+ return; // No resolvable owner to DM; the log above still records the edge.
15442
+ // Interactive card. `entered`: register a one-shot nonce, count the
15443
+ // machine-wide zombie/idle candidates so the buttons can show「(N)」
15444
+ // before a click, and send the stateful two-button card (clicking one
15445
+ // never removes the other — the handler rebuilds this same card).
15446
+ // `recovered`: display-only card. On any send failure, fall back to the
15447
+ // plain-text alert so the owner still hears about it.
15448
+ let cardJson;
15449
+ if (action.kind === 'entered') {
15450
+ const nonce = randomUUID();
15451
+ registerOverloadNonce(nonce);
15452
+ let counts = { stopped: 0, idle: 0 };
15453
+ try {
15454
+ counts = await countHostOverload();
15455
+ }
15456
+ catch (err) {
15457
+ logger.warn(`[overload] count failed, showing 0: ${err instanceof Error ? err.message : String(err)}`);
15458
+ }
15459
+ cardJson = buildOverloadAlertCard(initialOverloadCardState(action, counts, nonce));
15460
+ }
15461
+ else {
15462
+ cardJson = buildOverloadRecoveredCard(action);
15463
+ }
15464
+ void sendUserMessage(cfg.larkAppId, ownerOpenId, cardJson, 'interactive')
15465
+ .catch((err) => {
15466
+ logger.warn(`[overload] card DM failed, falling back to text: ${err instanceof Error ? err.message : String(err)}`);
15467
+ return sendUserMessage(cfg.larkAppId, ownerOpenId, formatOverloadAlert(action));
15468
+ })
15469
+ .catch((err) => logger.warn(`[overload] text fallback DM also failed: ${err instanceof Error ? err.message : String(err)}`));
15470
+ }
15471
+ catch (err) {
15472
+ logger.warn(`[overload] sample failed: ${err instanceof Error ? err.message : String(err)}`);
15473
+ }
15474
+ })();
15475
+ }, 30_000);
15476
+ overloadTimer.unref?.();
15477
+ }
14993
15478
  // Graceful shutdown. Sends SIGTERM (or `{type:'close'}` IPC via killWorker)
14994
15479
  // to every worker, then waits up to SHUTDOWN_GRACE_MS for them to exit
14995
15480
  // before sending SIGKILL to stragglers. Without the wait, daemon