botmux 3.5.0 → 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 (505) 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/backend/sandbox.d.ts +99 -279
  5. package/dist/adapters/backend/sandbox.d.ts.map +1 -1
  6. package/dist/adapters/backend/sandbox.js +424 -826
  7. package/dist/adapters/backend/sandbox.js.map +1 -1
  8. package/dist/adapters/cli/claude-code.d.ts +6 -0
  9. package/dist/adapters/cli/claude-code.d.ts.map +1 -1
  10. package/dist/adapters/cli/claude-code.js +197 -11
  11. package/dist/adapters/cli/claude-code.js.map +1 -1
  12. package/dist/adapters/cli/coco.js +4 -4
  13. package/dist/adapters/cli/coco.js.map +1 -1
  14. package/dist/adapters/cli/codex-app.d.ts.map +1 -1
  15. package/dist/adapters/cli/codex-app.js +9 -6
  16. package/dist/adapters/cli/codex-app.js.map +1 -1
  17. package/dist/adapters/cli/codex.d.ts.map +1 -1
  18. package/dist/adapters/cli/codex.js +9 -5
  19. package/dist/adapters/cli/codex.js.map +1 -1
  20. package/dist/adapters/cli/fs-policy.d.ts +244 -0
  21. package/dist/adapters/cli/fs-policy.d.ts.map +1 -0
  22. package/dist/adapters/cli/fs-policy.js +537 -0
  23. package/dist/adapters/cli/fs-policy.js.map +1 -0
  24. package/dist/adapters/cli/grok.d.ts +2 -1
  25. package/dist/adapters/cli/grok.d.ts.map +1 -1
  26. package/dist/adapters/cli/grok.js +2 -1
  27. package/dist/adapters/cli/grok.js.map +1 -1
  28. package/dist/adapters/cli/hermes.d.ts.map +1 -1
  29. package/dist/adapters/cli/hermes.js +8 -4
  30. package/dist/adapters/cli/hermes.js.map +1 -1
  31. package/dist/adapters/cli/mtr.d.ts.map +1 -1
  32. package/dist/adapters/cli/mtr.js +3 -2
  33. package/dist/adapters/cli/mtr.js.map +1 -1
  34. package/dist/adapters/cli/opencode.d.ts.map +1 -1
  35. package/dist/adapters/cli/opencode.js +5 -4
  36. package/dist/adapters/cli/opencode.js.map +1 -1
  37. package/dist/adapters/cli/read-isolation.d.ts +14 -200
  38. package/dist/adapters/cli/read-isolation.d.ts.map +1 -1
  39. package/dist/adapters/cli/read-isolation.js +18 -477
  40. package/dist/adapters/cli/read-isolation.js.map +1 -1
  41. package/dist/adapters/cli/relay.js +2 -2
  42. package/dist/adapters/cli/relay.js.map +1 -1
  43. package/dist/adapters/cli/runner-input.d.ts +2 -2
  44. package/dist/adapters/cli/runner-input.d.ts.map +1 -1
  45. package/dist/adapters/cli/runner-input.js +9 -6
  46. package/dist/adapters/cli/runner-input.js.map +1 -1
  47. package/dist/adapters/cli/seed.d.ts.map +1 -1
  48. package/dist/adapters/cli/seed.js +2 -1
  49. package/dist/adapters/cli/seed.js.map +1 -1
  50. package/dist/adapters/cli/traex.d.ts.map +1 -1
  51. package/dist/adapters/cli/traex.js +3 -2
  52. package/dist/adapters/cli/traex.js.map +1 -1
  53. package/dist/adapters/cli/types.d.ts +25 -18
  54. package/dist/adapters/cli/types.d.ts.map +1 -1
  55. package/dist/bot-registry.d.ts +54 -23
  56. package/dist/bot-registry.d.ts.map +1 -1
  57. package/dist/bot-registry.js +91 -0
  58. package/dist/bot-registry.js.map +1 -1
  59. package/dist/cli.d.ts.map +1 -1
  60. package/dist/cli.js +297 -104
  61. package/dist/cli.js.map +1 -1
  62. package/dist/codex-app-runner.js +85 -192
  63. package/dist/codex-app-runner.js.map +1 -1
  64. package/dist/config.d.ts +0 -16
  65. package/dist/config.d.ts.map +1 -1
  66. package/dist/config.js +0 -30
  67. package/dist/config.js.map +1 -1
  68. package/dist/core/ask-broker.d.ts +12 -0
  69. package/dist/core/ask-broker.d.ts.map +1 -1
  70. package/dist/core/ask-broker.js +41 -0
  71. package/dist/core/ask-broker.js.map +1 -1
  72. package/dist/core/bot-turn-mutation-gate.d.ts +24 -0
  73. package/dist/core/bot-turn-mutation-gate.d.ts.map +1 -0
  74. package/dist/core/bot-turn-mutation-gate.js +369 -0
  75. package/dist/core/bot-turn-mutation-gate.js.map +1 -0
  76. package/dist/core/command-handler.d.ts +5 -0
  77. package/dist/core/command-handler.d.ts.map +1 -1
  78. package/dist/core/command-handler.js +53 -73
  79. package/dist/core/command-handler.js.map +1 -1
  80. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  81. package/dist/core/dashboard-ipc-server.js +356 -84
  82. package/dist/core/dashboard-ipc-server.js.map +1 -1
  83. package/dist/core/dashboard-rows.d.ts +8 -0
  84. package/dist/core/dashboard-rows.d.ts.map +1 -1
  85. package/dist/core/dashboard-rows.js.map +1 -1
  86. package/dist/core/host-overload-alert.d.ts +157 -0
  87. package/dist/core/host-overload-alert.d.ts.map +1 -0
  88. package/dist/core/host-overload-alert.js +251 -0
  89. package/dist/core/host-overload-alert.js.map +1 -0
  90. package/dist/core/passthrough-commands.d.ts +8 -0
  91. package/dist/core/passthrough-commands.d.ts.map +1 -1
  92. package/dist/core/passthrough-commands.js +20 -1
  93. package/dist/core/passthrough-commands.js.map +1 -1
  94. package/dist/core/plugins/mcp/gateway-installer.d.ts.map +1 -1
  95. package/dist/core/plugins/mcp/gateway-installer.js +14 -2
  96. package/dist/core/plugins/mcp/gateway-installer.js.map +1 -1
  97. package/dist/core/restart-coordinator.d.ts +28 -0
  98. package/dist/core/restart-coordinator.d.ts.map +1 -0
  99. package/dist/core/restart-coordinator.js +82 -0
  100. package/dist/core/restart-coordinator.js.map +1 -0
  101. package/dist/core/restart-followup-policy.d.ts +112 -0
  102. package/dist/core/restart-followup-policy.d.ts.map +1 -0
  103. package/dist/core/restart-followup-policy.js +49 -0
  104. package/dist/core/restart-followup-policy.js.map +1 -0
  105. package/dist/core/role-library.d.ts +1 -1
  106. package/dist/core/role-library.js +1 -1
  107. package/dist/core/session-cwd.d.ts.map +1 -1
  108. package/dist/core/session-cwd.js +8 -0
  109. package/dist/core/session-cwd.js.map +1 -1
  110. package/dist/core/session-discovery.d.ts +14 -0
  111. package/dist/core/session-discovery.d.ts.map +1 -1
  112. package/dist/core/session-discovery.js +25 -0
  113. package/dist/core/session-discovery.js.map +1 -1
  114. package/dist/core/session-liveness.d.ts +15 -0
  115. package/dist/core/session-liveness.d.ts.map +1 -0
  116. package/dist/core/session-liveness.js +76 -0
  117. package/dist/core/session-liveness.js.map +1 -0
  118. package/dist/core/session-manager.d.ts.map +1 -1
  119. package/dist/core/session-manager.js +90 -60
  120. package/dist/core/session-manager.js.map +1 -1
  121. package/dist/core/session-row-enrichment.d.ts +15 -0
  122. package/dist/core/session-row-enrichment.d.ts.map +1 -0
  123. package/dist/core/session-row-enrichment.js +93 -0
  124. package/dist/core/session-row-enrichment.js.map +1 -0
  125. package/dist/core/slash-inject.js +1 -1
  126. package/dist/core/slash-inject.js.map +1 -1
  127. package/dist/core/trigger-final-suppression.d.ts +24 -0
  128. package/dist/core/trigger-final-suppression.d.ts.map +1 -0
  129. package/dist/core/trigger-final-suppression.js +97 -0
  130. package/dist/core/trigger-final-suppression.js.map +1 -0
  131. package/dist/core/trigger-session.d.ts.map +1 -1
  132. package/dist/core/trigger-session.js +64 -4
  133. package/dist/core/trigger-session.js.map +1 -1
  134. package/dist/core/types.d.ts +6 -0
  135. package/dist/core/types.d.ts.map +1 -1
  136. package/dist/core/types.js.map +1 -1
  137. package/dist/core/worker-pool.d.ts +44 -0
  138. package/dist/core/worker-pool.d.ts.map +1 -1
  139. package/dist/core/worker-pool.js +333 -38
  140. package/dist/core/worker-pool.js.map +1 -1
  141. package/dist/daemon.d.ts +5 -1
  142. package/dist/daemon.d.ts.map +1 -1
  143. package/dist/daemon.js +526 -35
  144. package/dist/daemon.js.map +1 -1
  145. package/dist/dashboard/aggregator.d.ts +1 -0
  146. package/dist/dashboard/aggregator.d.ts.map +1 -1
  147. package/dist/dashboard/aggregator.js +34 -6
  148. package/dist/dashboard/aggregator.js.map +1 -1
  149. package/dist/dashboard/bot-payload.d.ts +8 -0
  150. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  151. package/dist/dashboard/bot-payload.js +10 -0
  152. package/dist/dashboard/bot-payload.js.map +1 -1
  153. package/dist/dashboard/compat.d.ts +102 -2
  154. package/dist/dashboard/compat.d.ts.map +1 -1
  155. package/dist/dashboard/compat.js +78 -14
  156. package/dist/dashboard/compat.js.map +1 -1
  157. package/dist/dashboard/connector-api.d.ts.map +1 -1
  158. package/dist/dashboard/connector-api.js +1 -0
  159. package/dist/dashboard/connector-api.js.map +1 -1
  160. package/dist/dashboard/desktop-asks.d.ts +21 -0
  161. package/dist/dashboard/desktop-asks.d.ts.map +1 -0
  162. package/dist/dashboard/desktop-asks.js +49 -0
  163. package/dist/dashboard/desktop-asks.js.map +1 -0
  164. package/dist/dashboard/public-redact.d.ts +7 -0
  165. package/dist/dashboard/public-redact.d.ts.map +1 -1
  166. package/dist/dashboard/public-redact.js +35 -3
  167. package/dist/dashboard/public-redact.js.map +1 -1
  168. package/dist/dashboard/session-presentation.d.ts +16 -0
  169. package/dist/dashboard/session-presentation.d.ts.map +1 -0
  170. package/dist/dashboard/session-presentation.js +62 -0
  171. package/dist/dashboard/session-presentation.js.map +1 -0
  172. package/dist/dashboard/settings-write-applier.d.ts +2 -1
  173. package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
  174. package/dist/dashboard/settings-write-applier.js +19 -1
  175. package/dist/dashboard/settings-write-applier.js.map +1 -1
  176. package/dist/dashboard/trigger-api.d.ts.map +1 -1
  177. package/dist/dashboard/trigger-api.js +19 -1
  178. package/dist/dashboard/trigger-api.js.map +1 -1
  179. package/dist/dashboard/vc-consumer-profiles-api.d.ts +7 -2
  180. package/dist/dashboard/vc-consumer-profiles-api.d.ts.map +1 -1
  181. package/dist/dashboard/vc-consumer-profiles-api.js +13 -0
  182. package/dist/dashboard/vc-consumer-profiles-api.js.map +1 -1
  183. package/dist/dashboard/web/app.d.ts.map +1 -1
  184. package/dist/dashboard/web/app.js +18 -5
  185. package/dist/dashboard/web/app.js.map +1 -1
  186. package/dist/dashboard/web/bot-defaults-page.d.ts +16 -0
  187. package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
  188. package/dist/dashboard/web/bot-defaults-page.js +288 -7
  189. package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
  190. package/dist/dashboard/web/bot-defaults.d.ts +10 -0
  191. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  192. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  193. package/dist/dashboard/web/client-shell.d.ts +29 -0
  194. package/dist/dashboard/web/client-shell.d.ts.map +1 -0
  195. package/dist/dashboard/web/client-shell.js +89 -0
  196. package/dist/dashboard/web/client-shell.js.map +1 -0
  197. package/dist/dashboard/web/connectors-page.d.ts.map +1 -1
  198. package/dist/dashboard/web/connectors-page.js +5 -1
  199. package/dist/dashboard/web/connectors-page.js.map +1 -1
  200. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  201. package/dist/dashboard/web/i18n.js +130 -22
  202. package/dist/dashboard/web/i18n.js.map +1 -1
  203. package/dist/dashboard/web/preferences.d.ts +7 -0
  204. package/dist/dashboard/web/preferences.d.ts.map +1 -1
  205. package/dist/dashboard/web/preferences.js +32 -0
  206. package/dist/dashboard/web/preferences.js.map +1 -1
  207. package/dist/dashboard/web/sessions-kanban.d.ts +1 -1
  208. package/dist/dashboard/web/sessions-kanban.d.ts.map +1 -1
  209. package/dist/dashboard/web/sessions-kanban.js +1 -1
  210. package/dist/dashboard/web/sessions-kanban.js.map +1 -1
  211. package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
  212. package/dist/dashboard/web/sessions-page.js +78 -67
  213. package/dist/dashboard/web/sessions-page.js.map +1 -1
  214. package/dist/dashboard/web/sessions.d.ts +4 -0
  215. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  216. package/dist/dashboard/web/sessions.js +14 -1
  217. package/dist/dashboard/web/sessions.js.map +1 -1
  218. package/dist/dashboard/web/settings-page.d.ts +6 -0
  219. package/dist/dashboard/web/settings-page.d.ts.map +1 -1
  220. package/dist/dashboard/web/settings-page.js +55 -30
  221. package/dist/dashboard/web/settings-page.js.map +1 -1
  222. package/dist/dashboard/web/store.d.ts +1 -2
  223. package/dist/dashboard/web/store.d.ts.map +1 -1
  224. package/dist/dashboard/web/store.js +69 -20
  225. package/dist/dashboard/web/store.js.map +1 -1
  226. package/dist/dashboard/web/vc-consumer-profiles-section.d.ts.map +1 -1
  227. package/dist/dashboard/web/vc-consumer-profiles-section.js +128 -42
  228. package/dist/dashboard/web/vc-consumer-profiles-section.js.map +1 -1
  229. package/dist/dashboard/webhook-routes.d.ts.map +1 -1
  230. package/dist/dashboard/webhook-routes.js +9 -2
  231. package/dist/dashboard/webhook-routes.js.map +1 -1
  232. package/dist/dashboard-web/app.js +1 -1
  233. package/dist/dashboard-web/chunks/bot-defaults-page-VQMPUJ4Y.js +10 -0
  234. package/dist/dashboard-web/chunks/chunk-AGCPF6MT.js +1 -0
  235. package/dist/dashboard-web/chunks/{chunk-QCPPKCX3.js → chunk-JMRGZQDB.js} +1 -1
  236. package/dist/dashboard-web/chunks/{chunk-EJDYUINZ.js → chunk-NP4JUBGS.js} +1 -1
  237. package/dist/dashboard-web/chunks/{chunk-WXPMNEYV.js → chunk-P327LXOT.js} +1 -1
  238. package/dist/dashboard-web/chunks/chunk-SNKATZWK.js +49 -0
  239. package/dist/dashboard-web/chunks/connectors-page-DOY4J3ZL.js +1 -0
  240. package/dist/dashboard-web/chunks/{groups-page-3P4TLUMF.js → groups-page-74TS2DIH.js} +1 -1
  241. package/dist/dashboard-web/chunks/{insights-page-TDCZV56A.js → insights-page-AYCO65JC.js} +1 -1
  242. package/dist/dashboard-web/chunks/{monitor-room-QWCWL3SX.js → monitor-room-YKYLTSAN.js} +1 -1
  243. package/dist/dashboard-web/chunks/{monitoring-page-IOWFSNVC.js → monitoring-page-QBIUNRBR.js} +1 -1
  244. package/dist/dashboard-web/chunks/{office-page-5AUPEHZP.js → office-page-4PH2A26P.js} +1 -1
  245. package/dist/dashboard-web/chunks/overview-page-H73D5UT4.js +1 -0
  246. package/dist/dashboard-web/chunks/{roles-page-WJVB52ET.js → roles-page-U4BTOQ7F.js} +1 -1
  247. package/dist/dashboard-web/chunks/{schedules-page-RVRWHP4V.js → schedules-page-RHRHX7R7.js} +1 -1
  248. package/dist/dashboard-web/chunks/sessions-page-VSC6AM5G.js +2 -0
  249. package/dist/dashboard-web/chunks/settings-page-LF3RKM5P.js +2 -0
  250. package/dist/dashboard-web/chunks/{skills-page-7YT5TIX5.js → skills-page-SYOIGGXH.js} +1 -1
  251. package/dist/dashboard-web/chunks/{team-federation-page-NJZKTU7A.js → team-federation-page-RMBLASTW.js} +1 -1
  252. package/dist/dashboard-web/chunks/{v3-page-NQ73UA2Q.js → v3-page-XJXKKJRN.js} +1 -1
  253. package/dist/dashboard-web/chunks/{whiteboards-page-DSM3MDMN.js → whiteboards-page-K3YVSTHJ.js} +1 -1
  254. package/dist/dashboard-web/style.css +571 -3
  255. package/dist/dashboard.js +238 -9
  256. package/dist/dashboard.js.map +1 -1
  257. package/dist/global-config.d.ts +11 -0
  258. package/dist/global-config.d.ts.map +1 -1
  259. package/dist/global-config.js +27 -1
  260. package/dist/global-config.js.map +1 -1
  261. package/dist/i18n/en.d.ts.map +1 -1
  262. package/dist/i18n/en.js +6 -19
  263. package/dist/i18n/en.js.map +1 -1
  264. package/dist/i18n/zh.d.ts.map +1 -1
  265. package/dist/i18n/zh.js +6 -19
  266. package/dist/i18n/zh.js.map +1 -1
  267. package/dist/im/lark/card-builder.d.ts +1 -14
  268. package/dist/im/lark/card-builder.d.ts.map +1 -1
  269. package/dist/im/lark/card-builder.js +1 -35
  270. package/dist/im/lark/card-builder.js.map +1 -1
  271. package/dist/im/lark/card-handler.d.ts +10 -0
  272. package/dist/im/lark/card-handler.d.ts.map +1 -1
  273. package/dist/im/lark/card-handler.js +161 -44
  274. package/dist/im/lark/card-handler.js.map +1 -1
  275. package/dist/im/lark/client.d.ts +47 -0
  276. package/dist/im/lark/client.d.ts.map +1 -1
  277. package/dist/im/lark/client.js +127 -18
  278. package/dist/im/lark/client.js.map +1 -1
  279. package/dist/im/lark/event-dispatcher.d.ts +13 -0
  280. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  281. package/dist/im/lark/event-dispatcher.js +48 -3
  282. package/dist/im/lark/event-dispatcher.js.map +1 -1
  283. package/dist/im/lark/identity-cache.d.ts +25 -1
  284. package/dist/im/lark/identity-cache.d.ts.map +1 -1
  285. package/dist/im/lark/identity-cache.js +47 -1
  286. package/dist/im/lark/identity-cache.js.map +1 -1
  287. package/dist/im/lark/overload-nonce.d.ts +19 -0
  288. package/dist/im/lark/overload-nonce.d.ts.map +1 -0
  289. package/dist/im/lark/overload-nonce.js +72 -0
  290. package/dist/im/lark/overload-nonce.js.map +1 -0
  291. package/dist/im/lark/v3-progress-card.d.ts.map +1 -1
  292. package/dist/im/lark/v3-progress-card.js +5 -0
  293. package/dist/im/lark/v3-progress-card.js.map +1 -1
  294. package/dist/services/async-trigger-state.d.ts +82 -0
  295. package/dist/services/async-trigger-state.d.ts.map +1 -0
  296. package/dist/services/async-trigger-state.js +95 -0
  297. package/dist/services/async-trigger-state.js.map +1 -0
  298. package/dist/services/async-trigger-store.d.ts +28 -0
  299. package/dist/services/async-trigger-store.d.ts.map +1 -0
  300. package/dist/services/async-trigger-store.js +120 -0
  301. package/dist/services/async-trigger-store.js.map +1 -0
  302. package/dist/services/bot-config-store.d.ts.map +1 -1
  303. package/dist/services/bot-config-store.js +34 -2
  304. package/dist/services/bot-config-store.js.map +1 -1
  305. package/dist/services/bridge-fallback-gate.d.ts +32 -0
  306. package/dist/services/bridge-fallback-gate.d.ts.map +1 -1
  307. package/dist/services/bridge-fallback-gate.js +39 -0
  308. package/dist/services/bridge-fallback-gate.js.map +1 -1
  309. package/dist/services/bridge-turn-queue.d.ts.map +1 -1
  310. package/dist/services/bridge-turn-queue.js +15 -1
  311. package/dist/services/bridge-turn-queue.js.map +1 -1
  312. package/dist/services/card-prefs-store.d.ts +4 -0
  313. package/dist/services/card-prefs-store.d.ts.map +1 -1
  314. package/dist/services/card-prefs-store.js +8 -0
  315. package/dist/services/card-prefs-store.js.map +1 -1
  316. package/dist/services/claude-transcript.d.ts +23 -0
  317. package/dist/services/claude-transcript.d.ts.map +1 -1
  318. package/dist/services/claude-transcript.js +33 -0
  319. package/dist/services/claude-transcript.js.map +1 -1
  320. package/dist/services/codex-app-runner-protocol.d.ts +61 -0
  321. package/dist/services/codex-app-runner-protocol.d.ts.map +1 -0
  322. package/dist/services/codex-app-runner-protocol.js +170 -0
  323. package/dist/services/codex-app-runner-protocol.js.map +1 -0
  324. package/dist/services/codex-app-turn-controller.d.ts +72 -0
  325. package/dist/services/codex-app-turn-controller.d.ts.map +1 -0
  326. package/dist/services/codex-app-turn-controller.js +524 -0
  327. package/dist/services/codex-app-turn-controller.js.map +1 -0
  328. package/dist/services/codex-runner-freshness.d.ts +39 -0
  329. package/dist/services/codex-runner-freshness.d.ts.map +1 -0
  330. package/dist/services/codex-runner-freshness.js +78 -0
  331. package/dist/services/codex-runner-freshness.js.map +1 -0
  332. package/dist/services/connector-store.d.ts +6 -0
  333. package/dist/services/connector-store.d.ts.map +1 -1
  334. package/dist/services/connector-store.js.map +1 -1
  335. package/dist/services/grant-store.d.ts.map +1 -1
  336. package/dist/services/grant-store.js +16 -0
  337. package/dist/services/grant-store.js.map +1 -1
  338. package/dist/services/hermes-transcript.d.ts +1 -0
  339. package/dist/services/hermes-transcript.d.ts.map +1 -1
  340. package/dist/services/hermes-transcript.js +49 -0
  341. package/dist/services/hermes-transcript.js.map +1 -1
  342. package/dist/services/sandbox-migration.d.ts +14 -0
  343. package/dist/services/sandbox-migration.d.ts.map +1 -0
  344. package/dist/services/sandbox-migration.js +85 -0
  345. package/dist/services/sandbox-migration.js.map +1 -0
  346. package/dist/services/sandbox-store.d.ts +28 -0
  347. package/dist/services/sandbox-store.d.ts.map +1 -1
  348. package/dist/services/sandbox-store.js +82 -0
  349. package/dist/services/sandbox-store.js.map +1 -1
  350. package/dist/services/schedule-split-migration.d.ts +2 -0
  351. package/dist/services/schedule-split-migration.d.ts.map +1 -0
  352. package/dist/services/schedule-split-migration.js +159 -0
  353. package/dist/services/schedule-split-migration.js.map +1 -0
  354. package/dist/services/schedule-store.d.ts +28 -4
  355. package/dist/services/schedule-store.d.ts.map +1 -1
  356. package/dist/services/schedule-store.js +118 -33
  357. package/dist/services/schedule-store.js.map +1 -1
  358. package/dist/services/trigger-types.d.ts +18 -1
  359. package/dist/services/trigger-types.d.ts.map +1 -1
  360. package/dist/services/trigger-types.js +3 -0
  361. package/dist/services/trigger-types.js.map +1 -1
  362. package/dist/services/vc-meeting-consumer-profile-store.d.ts.map +1 -1
  363. package/dist/services/vc-meeting-consumer-profile-store.js +1 -0
  364. package/dist/services/vc-meeting-consumer-profile-store.js.map +1 -1
  365. package/dist/services/vc-meeting-consumer-profile-templates.d.ts +32 -0
  366. package/dist/services/vc-meeting-consumer-profile-templates.d.ts.map +1 -0
  367. package/dist/services/vc-meeting-consumer-profile-templates.js +106 -0
  368. package/dist/services/vc-meeting-consumer-profile-templates.js.map +1 -0
  369. package/dist/services/vc-meeting-delivery-feed.d.ts +1 -1
  370. package/dist/services/vc-meeting-delivery-feed.d.ts.map +1 -1
  371. package/dist/services/vc-meeting-delivery-feed.js +2 -1
  372. package/dist/services/vc-meeting-delivery-feed.js.map +1 -1
  373. package/dist/services/vc-meeting-delivery-hub-store.d.ts +3 -1
  374. package/dist/services/vc-meeting-delivery-hub-store.d.ts.map +1 -1
  375. package/dist/services/vc-meeting-delivery-hub-store.js +11 -0
  376. package/dist/services/vc-meeting-delivery-hub-store.js.map +1 -1
  377. package/dist/services/vc-meeting-delivery-protocol.d.ts +9 -2
  378. package/dist/services/vc-meeting-delivery-protocol.d.ts.map +1 -1
  379. package/dist/services/vc-meeting-delivery-protocol.js +10 -1
  380. package/dist/services/vc-meeting-delivery-protocol.js.map +1 -1
  381. package/dist/services/vc-meeting-delivery-receiver.d.ts.map +1 -1
  382. package/dist/services/vc-meeting-delivery-receiver.js +10 -1
  383. package/dist/services/vc-meeting-delivery-receiver.js.map +1 -1
  384. package/dist/services/vc-meeting-delivery-store.d.ts +12 -2
  385. package/dist/services/vc-meeting-delivery-store.d.ts.map +1 -1
  386. package/dist/services/vc-meeting-delivery-store.js +14 -2
  387. package/dist/services/vc-meeting-delivery-store.js.map +1 -1
  388. package/dist/services/vc-meeting-im-reply.d.ts +2 -1
  389. package/dist/services/vc-meeting-im-reply.d.ts.map +1 -1
  390. package/dist/services/vc-meeting-im-reply.js +19 -3
  391. package/dist/services/vc-meeting-im-reply.js.map +1 -1
  392. package/dist/services/vc-meeting-listener-output-protocol.d.ts +27 -0
  393. package/dist/services/vc-meeting-listener-output-protocol.d.ts.map +1 -0
  394. package/dist/services/vc-meeting-listener-output-protocol.js +58 -0
  395. package/dist/services/vc-meeting-listener-output-protocol.js.map +1 -0
  396. package/dist/services/vc-meeting-listener-topic-store.d.ts +31 -0
  397. package/dist/services/vc-meeting-listener-topic-store.d.ts.map +1 -0
  398. package/dist/services/vc-meeting-listener-topic-store.js +120 -0
  399. package/dist/services/vc-meeting-listener-topic-store.js.map +1 -0
  400. package/dist/services/vc-meeting-runtime-store.d.ts +2 -1
  401. package/dist/services/vc-meeting-runtime-store.d.ts.map +1 -1
  402. package/dist/services/vc-meeting-runtime-store.js +8 -0
  403. package/dist/services/vc-meeting-runtime-store.js.map +1 -1
  404. package/dist/services/vc-meeting-send-policy.d.ts +8 -1
  405. package/dist/services/vc-meeting-send-policy.d.ts.map +1 -1
  406. package/dist/services/vc-meeting-send-policy.js +8 -0
  407. package/dist/services/vc-meeting-send-policy.js.map +1 -1
  408. package/dist/types.d.ts +53 -4
  409. package/dist/types.d.ts.map +1 -1
  410. package/dist/utils/allowed-users-apply.d.ts +76 -0
  411. package/dist/utils/allowed-users-apply.d.ts.map +1 -0
  412. package/dist/utils/allowed-users-apply.js +123 -0
  413. package/dist/utils/allowed-users-apply.js.map +1 -0
  414. package/dist/utils/allowed-users-cache.d.ts +29 -0
  415. package/dist/utils/allowed-users-cache.d.ts.map +1 -0
  416. package/dist/utils/allowed-users-cache.js +82 -0
  417. package/dist/utils/allowed-users-cache.js.map +1 -0
  418. package/dist/utils/child-env.d.ts +4 -3
  419. package/dist/utils/child-env.d.ts.map +1 -1
  420. package/dist/utils/child-env.js +16 -1
  421. package/dist/utils/child-env.js.map +1 -1
  422. package/dist/utils/cli-usage-limit.d.ts +27 -1
  423. package/dist/utils/cli-usage-limit.d.ts.map +1 -1
  424. package/dist/utils/cli-usage-limit.js +92 -20
  425. package/dist/utils/cli-usage-limit.js.map +1 -1
  426. package/dist/utils/input-gate.d.ts +2 -0
  427. package/dist/utils/input-gate.d.ts.map +1 -1
  428. package/dist/utils/input-gate.js +2 -0
  429. package/dist/utils/input-gate.js.map +1 -1
  430. package/dist/utils/runtime-build-id.d.ts +21 -0
  431. package/dist/utils/runtime-build-id.d.ts.map +1 -0
  432. package/dist/utils/runtime-build-id.js +83 -0
  433. package/dist/utils/runtime-build-id.js.map +1 -0
  434. package/dist/utils/terminal-renderer.d.ts +3 -3
  435. package/dist/utils/terminal-renderer.js +3 -3
  436. package/dist/vc-agent/cards.d.ts.map +1 -1
  437. package/dist/vc-agent/cards.js +21 -1
  438. package/dist/vc-agent/cards.js.map +1 -1
  439. package/dist/worker.js +1010 -669
  440. package/dist/worker.js.map +1 -1
  441. package/dist/workflows/events/payloads.d.ts +56 -0
  442. package/dist/workflows/events/payloads.d.ts.map +1 -1
  443. package/dist/workflows/events/payloads.js +9 -0
  444. package/dist/workflows/events/payloads.js.map +1 -1
  445. package/dist/workflows/events/schema.d.ts +86 -0
  446. package/dist/workflows/events/schema.d.ts.map +1 -1
  447. package/dist/workflows/hostExecutors/botmux-schedule.d.ts.map +1 -1
  448. package/dist/workflows/hostExecutors/botmux-schedule.js +13 -1
  449. package/dist/workflows/hostExecutors/botmux-schedule.js.map +1 -1
  450. package/dist/workflows/migration/v2-run-archive-schema.d.ts +20 -20
  451. package/dist/workflows/migration/v2-to-v3.d.ts.map +1 -1
  452. package/dist/workflows/migration/v2-to-v3.js +5 -1
  453. package/dist/workflows/migration/v2-to-v3.js.map +1 -1
  454. package/dist/workflows/shared/sandbox-policy.d.ts +10 -1
  455. package/dist/workflows/shared/sandbox-policy.d.ts.map +1 -1
  456. package/dist/workflows/shared/sandbox-policy.js +12 -0
  457. package/dist/workflows/shared/sandbox-policy.js.map +1 -1
  458. package/dist/workflows/v3/architect.d.ts.map +1 -1
  459. package/dist/workflows/v3/architect.js +1 -0
  460. package/dist/workflows/v3/architect.js.map +1 -1
  461. package/dist/workflows/v3/bot-resolve.d.ts.map +1 -1
  462. package/dist/workflows/v3/bot-resolve.js +36 -0
  463. package/dist/workflows/v3/bot-resolve.js.map +1 -1
  464. package/dist/workflows/v3/cli-run.d.ts +9 -0
  465. package/dist/workflows/v3/cli-run.d.ts.map +1 -1
  466. package/dist/workflows/v3/cli-run.js +17 -2
  467. package/dist/workflows/v3/cli-run.js.map +1 -1
  468. package/dist/workflows/v3/contract.d.ts +8 -0
  469. package/dist/workflows/v3/contract.d.ts.map +1 -1
  470. package/dist/workflows/v3/contract.js.map +1 -1
  471. package/dist/workflows/v3/goal-cli.d.ts +58 -0
  472. package/dist/workflows/v3/goal-cli.d.ts.map +1 -0
  473. package/dist/workflows/v3/goal-cli.js +430 -0
  474. package/dist/workflows/v3/goal-cli.js.map +1 -0
  475. package/dist/workflows/v3/library-materialize.d.ts.map +1 -1
  476. package/dist/workflows/v3/library-materialize.js +5 -1
  477. package/dist/workflows/v3/library-materialize.js.map +1 -1
  478. package/dist/workflows/v3/library-schema.d.ts +21 -0
  479. package/dist/workflows/v3/library-schema.d.ts.map +1 -1
  480. package/dist/workflows/v3/library-schema.js +95 -1
  481. package/dist/workflows/v3/library-schema.js.map +1 -1
  482. package/dist/workflows/v3/progress-projection.d.ts +3 -0
  483. package/dist/workflows/v3/progress-projection.d.ts.map +1 -1
  484. package/dist/workflows/v3/progress-projection.js +23 -1
  485. package/dist/workflows/v3/progress-projection.js.map +1 -1
  486. package/dist/workflows/v3/run-envelope.d.ts +46 -1
  487. package/dist/workflows/v3/run-envelope.d.ts.map +1 -1
  488. package/dist/workflows/v3/run-envelope.js +32 -1
  489. package/dist/workflows/v3/run-envelope.js.map +1 -1
  490. package/package.json +2 -2
  491. package/dist/dashboard-web/chunks/bot-defaults-page-O376WSER.js +0 -5
  492. package/dist/dashboard-web/chunks/chunk-H7P5UV4W.js +0 -49
  493. package/dist/dashboard-web/chunks/chunk-SZOB6VK2.js +0 -1
  494. package/dist/dashboard-web/chunks/connectors-page-IR54AOAI.js +0 -1
  495. package/dist/dashboard-web/chunks/overview-page-QJMI6NLW.js +0 -1
  496. package/dist/dashboard-web/chunks/sessions-page-KWLGBX6G.js +0 -3
  497. package/dist/dashboard-web/chunks/settings-page-TNZEPC7V.js +0 -2
  498. package/dist/services/sandbox-land.d.ts +0 -45
  499. package/dist/services/sandbox-land.d.ts.map +0 -1
  500. package/dist/services/sandbox-land.js +0 -394
  501. package/dist/services/sandbox-land.js.map +0 -1
  502. package/dist/utils/screen-analyzer.d.ts +0 -75
  503. package/dist/utils/screen-analyzer.d.ts.map +0 -1
  504. package/dist/utils/screen-analyzer.js +0 -333
  505. 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,19 +15,24 @@ 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';
30
+ import { migrateSandboxConfigAtStartup } from './services/sandbox-migration.js';
26
31
  import * as sessionStore from './services/session-store.js';
27
32
  import * as chatFirstSeenStore from './services/chat-first-seen-store.js';
28
33
  import { ensureDefaultOncallBound } from './services/oncall-store.js';
29
34
  import * as scheduleStore from './services/schedule-store.js';
35
+ import { migrateSharedSchedulesAtStartup } from './services/schedule-split-migration.js';
30
36
  import * as messageQueue from './services/message-queue.js';
31
37
  import { emitHookEvent, emitHookEventLocal, HOOK_EVENTS } from './services/hook-runner.js';
32
38
  import { setSessionLifecycleShutdown } from './services/session-lifecycle-hooks.js';
@@ -35,7 +41,8 @@ import { expandMergeForward } from './im/lark/merge-forward.js';
35
41
  import { bindResourcesToMessage, composeForwardFollowupContent, mergeMessageMentions } from './im/lark/forward-followup-content.js';
36
42
  import { buildQuoteHint } from './im/lark/quote-hint.js';
37
43
  import { logger } from './utils/logger.js';
38
- 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';
39
46
  import { delay } from './utils/timing.js';
40
47
  import { BoundedMap } from './utils/bounded-map.js';
41
48
  import { checkAllowedChatGroupsConfig } from './services/allowed-chat-groups.js';
@@ -115,8 +122,30 @@ let selfV3BootInstanceId;
115
122
  /** Generic daemon identity used by internal receiver endpoints. Unlike the
116
123
  * VC listener switch, every agent daemon may receive a fenced membership. */
117
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
+ }
118
147
  let vcMeetingTerminalReconciler;
119
- 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';
120
149
  import { getDocSubscription, listAllDocSubscriptions, removeDocSubscription, setDocCommentPollCursor } from './services/doc-subs-store.js';
121
150
  import { BOT_REPLY_SENTINEL, subscribeDocFile, unsubscribeDocFile, addCommentReaction, hasBotSentinel, isBotAuthoredReply, listDocComments } from './im/lark/doc-comment.js';
122
151
  import { learnFromMentions, resolveSender, flushIdentityCacheSync } from './im/lark/identity-cache.js';
@@ -142,7 +171,7 @@ import { createGroupWithBots } from './services/group-creator.js';
142
171
  import { addBotToChat, isInChat } from './services/groups-store.js';
143
172
  import { setChatReplyMode } from './services/chat-reply-mode-store.js';
144
173
  import { hasVcMeetingEndedTombstone, listVcMeetingRuntimeSessionsByListenerAndAgent, listVcMeetingRuntimeSessions, pruneExpiredVcMeetingRuntimeSessions, recordVcMeetingEndedTombstone, recordVcMeetingRuntimeSession, removeVcMeetingRuntimeSession, } from './services/vc-meeting-runtime-store.js';
145
- import { computeVcMeetingConsumerProfileHash } from './services/vc-meeting-profile-instructions.js';
174
+ import { computeVcMeetingConsumerProfileHash, normalizeVcMeetingProfileInstructions, } from './services/vc-meeting-profile-instructions.js';
146
175
  import { bootstrapVcMeetingDefaultConsumerProfile } from './services/vc-meeting-consumer-profile-bootstrap.js';
147
176
  import { getVcMeetingPreparation, normalizeVcMeetingNumber, } from './services/vc-meeting-preparations-store.js';
148
177
  import { abandonPoisonedVcMeetingDelivery, getVcMeetingDeliveryStatus, handleVcMeetingTurnTerminal, handleVcMeetingWorkerGenerationExit, receiveVcMeetingDelivery, registerVcMeetingMember, retryPoisonedVcMeetingDelivery, } from './services/vc-meeting-delivery-receiver.js';
@@ -153,6 +182,7 @@ import { applyVcMeetingHubMemberProjection, freezeVcMeetingHubDeliveryAssignment
153
182
  import { getVcMeetingFeedMetadataState, ingestVcMeetingFeedMetadata, } from './services/vc-meeting-feed-metadata-store.js';
154
183
  import { VcMeetingTerminalReconciler } from './services/vc-meeting-terminal-reconciler.js';
155
184
  import { resolveVcMeetingImTurnOrigin, verifyVcMeetingManagedOriginClaim, } from './services/vc-meeting-send-policy.js';
185
+ import { ensureVcMeetingListenerTopicRoot, } from './services/vc-meeting-listener-topic-store.js';
156
186
  import { finishVcMeetingManagedActionProvider, finishVcMeetingManagedApprovalCard, requestVcMeetingManagedImAction, requestVcMeetingManagedAction, resolveVcMeetingManagedActionApproval, } from './services/vc-meeting-action-gate.js';
157
187
  import { claimVcMeetingApprovalCardAttempt, findVcMeetingAction, listVcMeetingActionScopes, listVcMeetingActions, reconcileVcMeetingActionsOnBoot, } from './services/vc-meeting-action-store.js';
158
188
  import { authorizeVcMeetingDaemonControlRequest, ensureVcMeetingDaemonAuthToken, withVcMeetingDaemonAuthHeader, } from './services/vc-meeting-daemon-auth.js';
@@ -802,6 +832,8 @@ const VC_MEETING_SYNC_INTERVAL_OPTIONS_MS = [15_000, 30_000, 60_000, 90_000];
802
832
  const VC_MEETING_CUSTOM_SYNC_INTERVAL_MIN_SECONDS = 10;
803
833
  const VC_MEETING_CUSTOM_SYNC_INTERVAL_MAX_SECONDS = 3_600;
804
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;
805
837
  const vcMeetingEndedTombstones = new BoundedMap(2_000);
806
838
  const vcMeetingPendingInvites = new BoundedMap(2_000);
807
839
  function vcMeetingSessionKey(larkAppId, meetingId) {
@@ -898,6 +930,26 @@ function normalizeVcMeetingCustomSyncIntervalMs(value) {
898
930
  function vcMeetingCustomSyncIntervalFromCard(data) {
899
931
  return normalizeVcMeetingCustomSyncIntervalMs(data.action?.form_value?.[VC_MEETING_CUSTOM_SYNC_INTERVAL_FIELD]);
900
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
+ }
901
953
  function vcMeetingSessionFlushIntervalMs(session, cfg) {
902
954
  return session.syncIntervalMs ?? cfg.flushIntervalMs ?? DEFAULT_VC_MEETING_FLUSH_INTERVAL_MS;
903
955
  }
@@ -1888,6 +1940,20 @@ async function sessionReply(anchor, content, msgType = 'text', larkAppId, turnId
1888
1940
  return sendWithHookPolicy(chatId, content, msgType, opts.uuid);
1889
1941
  }
1890
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
+ }
1891
1957
  if (ds?.scope === 'chat') {
1892
1958
  const fresh = readSessionFreshFromDisk(ds.session.sessionId, ds.larkAppId);
1893
1959
  if (fresh)
@@ -2102,6 +2168,123 @@ function removeDaemonDescriptor(larkAppId) {
2102
2168
  catch { /* ignore */ }
2103
2169
  }
2104
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
+ }
2105
2288
  // ─── Version tracking ────────────────────────────────────────────────────────
2106
2289
  function refreshCliVersion(cliId, cliPathOverride) {
2107
2290
  const now = Date.now();
@@ -7580,11 +7763,29 @@ function vcMeetingRuntimeAgentForProfile(profile, status, opts = {}) {
7580
7763
  ...(opts.activationError ? { activationError: opts.activationError.slice(0, 500) } : {}),
7581
7764
  ...(filter ? { filter } : {}),
7582
7765
  responseMode: profile.responseMode,
7766
+ listenerPlacement: profile.listenerDelivery?.placement ?? 'auto',
7583
7767
  capabilities: vcMeetingCanonicalProfileCapabilities(profile),
7584
7768
  ownedSinks: vcMeetingCanonicalProfileOwnedSinks(profile),
7585
7769
  deliveryProfileHash: opts.deliveryProfileHash ?? vcMeetingConsumerProfileHash(profile),
7586
7770
  };
7587
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
+ }
7588
7789
  /** Rehydrate the exact immutable profile snapshot selected for this member
7589
7790
  * epoch. Current bots.json is only authoritative for a first join/re-add; an
7590
7791
  * edit or deletion must not rewrite an in-flight meeting after restart. */
@@ -7597,6 +7798,9 @@ function vcMeetingProfileFromRuntimeAgent(selected) {
7597
7798
  ...(selected.instructions ? { instructions: selected.instructions } : {}),
7598
7799
  ...(selected.filter ? { filter: selected.filter } : {}),
7599
7800
  responseMode: selected.responseMode,
7801
+ ...(selected.listenerPlacement !== 'auto'
7802
+ ? { listenerDelivery: { placement: selected.listenerPlacement } }
7803
+ : {}),
7600
7804
  capabilities: [...selected.capabilities],
7601
7805
  ...(selected.ownedSinks.length > 0 ? { ownedSinks: [...selected.ownedSinks] } : {}),
7602
7806
  };
@@ -7664,6 +7868,7 @@ async function ensureVcMeetingProfileMember(session, profile, listenerChatId, op
7664
7868
  && JSON.stringify(prior.ownedSinks) === JSON.stringify(canonicalOwnedSinks);
7665
7869
  const samePolicy = sameStreamSemantics
7666
7870
  && prior.responseMode === profile.responseMode
7871
+ && prior.outputPlacement === (profile.listenerDelivery?.placement ?? 'auto')
7667
7872
  && JSON.stringify(prior.capabilities) === JSON.stringify(canonicalCapabilities)
7668
7873
  && sameOwnedSinks;
7669
7874
  // Receiver registration commits before the hub projection. A crash in that
@@ -7680,6 +7885,7 @@ async function ensureVcMeetingProfileMember(session, profile, listenerChatId, op
7680
7885
  && receiverPrior.instructions === instructions
7681
7886
  && receiverPrior.outputChatId === listenerChatId
7682
7887
  && receiverPrior.responseMode === profile.responseMode
7888
+ && receiverPrior.outputPlacement === (profile.listenerDelivery?.placement ?? 'auto')
7683
7889
  && JSON.stringify(receiverPrior.filter ?? {}) === JSON.stringify(canonicalFilter ?? {})
7684
7890
  && JSON.stringify(receiverPrior.capabilities) === JSON.stringify(canonicalCapabilities)
7685
7891
  && JSON.stringify(receiverPrior.ownedSinks) === JSON.stringify(canonicalOwnedSinks)
@@ -7744,7 +7950,10 @@ async function ensureVcMeetingProfileMember(session, profile, listenerChatId, op
7744
7950
  responseMode: orphanedReceiver?.responseMode ?? profile.responseMode,
7745
7951
  ...policy,
7746
7952
  },
7747
- outputRoute: { chatId: orphanedReceiver?.outputChatId ?? listenerChatId },
7953
+ outputRoute: {
7954
+ chatId: orphanedReceiver?.outputChatId ?? listenerChatId,
7955
+ placement: orphanedReceiver?.outputPlacement ?? profile.listenerDelivery?.placement ?? 'auto',
7956
+ },
7748
7957
  };
7749
7958
  const receiver = await postVcMeetingMemberProjection(profile.agentAppId, projection);
7750
7959
  const body = vcMeetingResponseRecord(receiver.body);
@@ -7777,6 +7986,7 @@ async function ensureVcMeetingProfileMember(session, profile, listenerChatId, op
7777
7986
  membershipGeneration,
7778
7987
  status: 'active',
7779
7988
  responseMode: orphanedReceiver?.responseMode ?? profile.responseMode,
7989
+ outputPlacement: orphanedReceiver?.outputPlacement ?? profile.listenerDelivery?.placement ?? 'auto',
7780
7990
  ...policy,
7781
7991
  joinedAtIngestSeq,
7782
7992
  receiverSessionId,
@@ -7889,6 +8099,7 @@ async function pauseVcMeetingSingleConsumerMembership(session) {
7889
8099
  membershipGeneration,
7890
8100
  status: 'paused',
7891
8101
  responseMode: prior.responseMode,
8102
+ outputPlacement: prior.outputPlacement ?? 'auto',
7892
8103
  joinedAtIngestSeq: prior.joinedAtIngestSeq,
7893
8104
  receiverSessionId: prior.receiverSessionId,
7894
8105
  outputChatId: prior.outputChatId,
@@ -7914,7 +8125,7 @@ async function pauseVcMeetingSingleConsumerMembership(session) {
7914
8125
  joinedAtIngestSeq: prior.joinedAtIngestSeq,
7915
8126
  responseMode: prior.responseMode,
7916
8127
  },
7917
- outputRoute: { chatId: prior.outputChatId },
8128
+ outputRoute: { chatId: prior.outputChatId, placement: prior.outputPlacement ?? 'auto' },
7918
8129
  });
7919
8130
  const body = vcMeetingResponseRecord(receiver.body);
7920
8131
  if (receiver.status < 200 || receiver.status >= 300 || body?.ok !== true) {
@@ -7960,6 +8171,7 @@ async function pauseVcMeetingProfileMembership(session, selected) {
7960
8171
  membershipGeneration,
7961
8172
  status: 'paused',
7962
8173
  responseMode: prior.responseMode,
8174
+ outputPlacement: prior.outputPlacement ?? 'auto',
7963
8175
  ...policy,
7964
8176
  joinedAtIngestSeq: prior.joinedAtIngestSeq,
7965
8177
  receiverSessionId: prior.receiverSessionId,
@@ -7987,7 +8199,7 @@ async function pauseVcMeetingProfileMembership(session, selected) {
7987
8199
  responseMode: prior.responseMode,
7988
8200
  ...policy,
7989
8201
  },
7990
- outputRoute: { chatId: prior.outputChatId },
8202
+ outputRoute: { chatId: prior.outputChatId, placement: prior.outputPlacement ?? 'auto' },
7991
8203
  });
7992
8204
  const body = vcMeetingResponseRecord(receiver.body);
7993
8205
  if (receiver.status < 200 || receiver.status >= 300 || body?.ok !== true) {
@@ -8018,6 +8230,7 @@ function retireVcMeetingSingleConsumerForRecovery(session, reason) {
8018
8230
  membershipGeneration,
8019
8231
  status: 'removed',
8020
8232
  responseMode: member.responseMode,
8233
+ outputPlacement: member.outputPlacement ?? 'auto',
8021
8234
  joinedAtIngestSeq: member.joinedAtIngestSeq,
8022
8235
  receiverSessionId: member.receiverSessionId,
8023
8236
  outputChatId: member.outputChatId,
@@ -8037,6 +8250,7 @@ function retireVcMeetingSingleConsumerForRecovery(session, reason) {
8037
8250
  membershipGeneration,
8038
8251
  status: 'removed',
8039
8252
  responseMode: member.responseMode,
8253
+ outputPlacement: member.outputPlacement ?? 'auto',
8040
8254
  joinedAtIngestSeq: member.joinedAtIngestSeq,
8041
8255
  receiverSessionId: member.receiverSessionId,
8042
8256
  outputChatId: member.outputChatId,
@@ -8078,6 +8292,7 @@ function retireVcMeetingProfileMemberForRecovery(member, reason) {
8078
8292
  membershipGeneration,
8079
8293
  status: 'removed',
8080
8294
  responseMode: member.responseMode,
8295
+ outputPlacement: member.outputPlacement ?? 'auto',
8081
8296
  ...policy,
8082
8297
  joinedAtIngestSeq: member.joinedAtIngestSeq,
8083
8298
  receiverSessionId: member.receiverSessionId,
@@ -8099,6 +8314,7 @@ function retireVcMeetingProfileMemberForRecovery(member, reason) {
8099
8314
  membershipGeneration,
8100
8315
  status: 'removed',
8101
8316
  responseMode: member.responseMode,
8317
+ outputPlacement: member.outputPlacement ?? 'auto',
8102
8318
  ...policy,
8103
8319
  joinedAtIngestSeq: member.joinedAtIngestSeq,
8104
8320
  receiverSessionId: member.receiverSessionId,
@@ -8269,6 +8485,7 @@ function vcMeetingProfileMemberSemanticsMatchRuntime(member, selected, listenerC
8269
8485
  && member.outputChatId === listenerChatId
8270
8486
  && member.deliveryProfileHash === selected.deliveryProfileHash
8271
8487
  && member.responseMode === selected.responseMode
8488
+ && (member.outputPlacement ?? 'auto') === selected.listenerPlacement
8272
8489
  && JSON.stringify(member.filter ?? {}) === JSON.stringify(selected.filter ?? {})
8273
8490
  && JSON.stringify(member.capabilities) === JSON.stringify(selected.capabilities)
8274
8491
  && JSON.stringify(member.ownedSinks) === JSON.stringify(selected.ownedSinks);
@@ -8310,6 +8527,7 @@ function refreshVcMeetingProfileOwnerBoot(session) {
8310
8527
  joinedAtIngestSeq: member.joinedAtIngestSeq,
8311
8528
  receiverSessionId: member.receiverSessionId,
8312
8529
  outputChatId: member.outputChatId,
8530
+ outputPlacement: member.outputPlacement ?? 'auto',
8313
8531
  });
8314
8532
  if (!applied.ok)
8315
8533
  throw new Error(`receiver ${member.memberId}: ${applied.reason}`);
@@ -8335,6 +8553,7 @@ function refreshVcMeetingProfileOwnerBoot(session) {
8335
8553
  membershipGeneration: member.membershipGeneration,
8336
8554
  status: member.status,
8337
8555
  responseMode: member.responseMode,
8556
+ outputPlacement: member.outputPlacement ?? 'auto',
8338
8557
  ...policy,
8339
8558
  joinedAtIngestSeq: member.joinedAtIngestSeq,
8340
8559
  receiverSessionId: member.receiverSessionId,
@@ -9515,7 +9734,7 @@ async function removeVcMeetingProfileMember(session, selected) {
9515
9734
  ownedSinks: prior.ownedSinks,
9516
9735
  sinkOwnerGeneration: prior.sinkOwnerGeneration,
9517
9736
  },
9518
- outputRoute: { chatId: prior.outputChatId },
9737
+ outputRoute: { chatId: prior.outputChatId, placement: prior.outputPlacement ?? 'auto' },
9519
9738
  };
9520
9739
  const receiver = await postVcMeetingMemberProjection(prior.agentAppId, projection);
9521
9740
  const body = vcMeetingResponseRecord(receiver.body);
@@ -9536,6 +9755,7 @@ async function removeVcMeetingProfileMember(session, selected) {
9536
9755
  membershipGeneration,
9537
9756
  status: 'removed',
9538
9757
  responseMode: prior.responseMode,
9758
+ outputPlacement: prior.outputPlacement ?? 'auto',
9539
9759
  ...(prior.filter ? { filter: prior.filter } : {}),
9540
9760
  capabilities: prior.capabilities,
9541
9761
  ownedSinks: prior.ownedSinks,
@@ -9742,7 +9962,9 @@ async function applyVcMeetingConsumerProfileSelection(key, session, cfg, selecte
9742
9962
  const existingByProfileId = new Map(session.selectedAgents.map(selected => [selected.profileId, selected]));
9743
9963
  const requestedProfiles = resolution.selectedProfiles.map((profile) => {
9744
9964
  const existing = existingByProfileId.get(profile.id);
9745
- return existing ? vcMeetingProfileFromRuntimeAgent(existing) : profile;
9965
+ return existing
9966
+ ? vcMeetingProfileFromRuntimeAgent(existing)
9967
+ : vcMeetingProfileWithActivationContext(profile, opts.activationContext);
9746
9968
  });
9747
9969
  const retainedAfterRemovalFailure = [];
9748
9970
  const removalErrors = [];
@@ -9835,6 +10057,7 @@ async function applyVcMeetingConsumerProfileSelection(key, session, cfg, selecte
9835
10057
  session.consumerSelectionApplying = false;
9836
10058
  session.consumerPendingProfileIds = undefined;
9837
10059
  session.consumerPendingIntervalMs = undefined;
10060
+ session.consumerPendingActivationContext = undefined;
9838
10061
  }
9839
10062
  }
9840
10063
  async function activateVcMeetingPreparationBinding(session, cfg, preparation) {
@@ -9982,9 +10205,10 @@ async function applyVcMeetingConsumerProfileStagedState(key, session, cfg, fallb
9982
10205
  const selectedProfileIds = session.consumerPendingProfileIds
9983
10206
  ?? (fallbackProfileIds ? [...fallbackProfileIds] : undefined);
9984
10207
  const stagedIntervalMs = session.consumerPendingIntervalMs;
10208
+ const activationContext = session.consumerPendingActivationContext;
9985
10209
  if (stagedIntervalMs)
9986
10210
  session.syncIntervalMs = stagedIntervalMs;
9987
- let result = await applyVcMeetingConsumerProfileSelection(key, session, cfg, selectedProfileIds, opts);
10211
+ let result = await applyVcMeetingConsumerProfileSelection(key, session, cfg, selectedProfileIds, { ...opts, ...(activationContext ? { activationContext } : {}) });
9988
10212
  if (!result.ok && selectedProfileIds === undefined && session.consumerMode === 'pending') {
9989
10213
  // A conflicting/invalid hand-edited default must never leave a pending
9990
10214
  // timer retrying or partially activate a subset. Fail closed to
@@ -11565,6 +11789,11 @@ async function handleVcMeetingCardAction(data, larkAppId) {
11565
11789
  session.consumerSelectionApplying = false;
11566
11790
  }
11567
11791
  }
11792
+ const activationContext = vcMeetingActivationContextFromCard(data);
11793
+ if (!activationContext.ok) {
11794
+ return { toast: { type: 'error', content: activationContext.error } };
11795
+ }
11796
+ session.consumerPendingActivationContext = activationContext.context;
11568
11797
  return applyVcMeetingConsumerSelectionInBackground(key, session, cfg, () => applyVcMeetingConsumerProfileStagedState(key, session, cfg, undefined, { claimed: true }), { cardMessageId: data.context?.open_message_id ?? data.open_message_id });
11569
11798
  }
11570
11799
  const selection = session.consumerPendingChoice ?? { mode: 'default' };
@@ -12193,7 +12422,7 @@ async function startInitialPassthroughSession(args) {
12193
12422
  const botCfg = getBot(larkAppId).config;
12194
12423
  refreshCliVersion(botCfg.cliId, botCfg.cliPathOverride);
12195
12424
  const directChatSender = chatType === 'p2p'
12196
- ? await resolveSender(larkAppId, senderOpenId, parsed.senderType)
12425
+ ? await resolveSender(larkAppId, senderOpenId, parsed.senderType, { messageId })
12197
12426
  : undefined;
12198
12427
  // Group cold-start passthroughs only need a stable caller identity while the
12199
12428
  // repo picker is pending. Avoid adding a contact lookup to every /goal start;
@@ -12512,7 +12741,9 @@ async function handleNewTopic(data, ctx) {
12512
12741
  // chat-granted users (who only pass canTalk) management commands like
12513
12742
  // /cd /restart /oncall bind. Previously this gate only fired in oncall chats,
12514
12743
  // which left a hole once per-chat grants flow through canTalk.
12515
- 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)) {
12516
12747
  await sessionReply(anchor, tr('daemon.cmd_allowed_users_only', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
12517
12748
  return;
12518
12749
  }
@@ -12546,7 +12777,7 @@ async function handleNewTopic(data, ctx) {
12546
12777
  const session = sessionStore.createSession(chatId, cmdRootIdForStore, cmdContent.substring(0, 50), chatType);
12547
12778
  const now = Date.now();
12548
12779
  if (chatType === 'p2p') {
12549
- setDirectChatDisplayNameFromSender(session, chatType, await resolveSender(larkAppId, senderOpenId, parsed.senderType));
12780
+ setDirectChatDisplayNameFromSender(session, chatType, await resolveSender(larkAppId, senderOpenId, parsed.senderType, { messageId }));
12550
12781
  }
12551
12782
  session.larkAppId = larkAppId;
12552
12783
  session.ownerOpenId = senderOpenId;
@@ -12621,7 +12852,7 @@ async function handleNewTopic(data, ctx) {
12621
12852
  // resolveSender for an unseen open_id may await contact.v3.user.get with a
12622
12853
  // short budget; subsequent calls hit the cache and are sync-fast.
12623
12854
  const groupChatNamePromise = resolveGroupChatNameForNativeTitle(larkAppId, chatId, chatType, botCfg.cliId, parsed.content, parsed.mentions);
12624
- const newTopicSender = await resolveSender(larkAppId, senderOpenId, parsed.senderType);
12855
+ const newTopicSender = await resolveSender(larkAppId, senderOpenId, parsed.senderType, { messageId });
12625
12856
  refreshCliVersion(botCfg.cliId, botCfg.cliPathOverride);
12626
12857
  // Pin the working dir via the layered oncall / inherit / default lookup
12627
12858
  // (auto-binds a defaultOncall chat as a side effect). Shared with the
@@ -13083,7 +13314,9 @@ async function handleThreadReply(data, ctx) {
13083
13314
  if (threadSenderResolved)
13084
13315
  return threadSenderCached;
13085
13316
  threadSenderResolved = true;
13086
- 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 });
13087
13320
  return threadSenderCached;
13088
13321
  };
13089
13322
  const content = parsed.content.trim();
@@ -13285,7 +13518,9 @@ async function handleThreadReply(data, ctx) {
13285
13518
  }
13286
13519
  // canOperate gate for thread-reply daemon commands — required in every chat
13287
13520
  // (see spawn-path gate above). Denies chat-granted users management commands.
13288
- if (!canOperate(larkAppId, effectiveThreadChatId, threadSenderOpenId, threadTeamTrustUnionId)) {
13521
+ // canRunDaemonCommand:canTalkDaemonCommands 名单内的命令降到 canTalk,
13522
+ // 与 new-topic 路径的统一闸同款(未配置时与 canOperate 全等)。
13523
+ if (!canRunDaemonCommand(larkAppId, effectiveThreadChatId, threadSenderOpenId, threadTeamTrustUnionId, cmd, threadSenderUnionId, ctxChatType)) {
13289
13524
  sessionReply(anchor, tr('daemon.cmd_allowed_users_only', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
13290
13525
  return;
13291
13526
  }
@@ -14198,6 +14433,111 @@ function resolvePrimaryOwnerOpenId(larkAppId) {
14198
14433
  return undefined;
14199
14434
  }
14200
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
+ }
14201
14541
  /** Build the current dashboard URL (active token, not a rotation) from the
14202
14542
  * dashboard process's persisted `.dashboard-port` / `.dashboard-token`. Falls
14203
14543
  * back to a token-less base URL if the dashboard hasn't published a token yet. */
@@ -14253,6 +14593,11 @@ export async function startDaemon(botIndex) {
14253
14593
  // 首次启动时后台尝试安装 CJK 字体(Debian/Ubuntu),避免截图中文显示豆腐块。
14254
14594
  // 不阻塞:首张截图可能仍是豆腐块,装完重启 daemon 即可正常。
14255
14595
  ensureCjkFontsInstalled();
14596
+ // Auto-migrate legacy sandbox fields (readIsolation / sandboxHidePaths /
14597
+ // sandboxReadonlyPaths / readDenyExtraPaths → sandbox + sandboxPaths) BEFORE
14598
+ // loading, so the parsed configs below already carry the new model. Writes
14599
+ // new fields, keeps old ones (downgrade = zero-op), backs up once. Idempotent.
14600
+ await migrateSandboxConfigAtStartup();
14256
14601
  // Load the assigned bot (one daemon per bot)
14257
14602
  let botConfigs = loadBotConfigs();
14258
14603
  const idx = botIndex ?? 0;
@@ -14393,8 +14738,12 @@ export async function startDaemon(botIndex) {
14393
14738
  }
14394
14739
  }, VC_MEETING_DELIVERY_LEASE_SCAN_MS);
14395
14740
  vcMeetingDeliveryLeaseTimer.unref?.();
14396
- // 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`
14397
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);
14398
14747
  scheduleStore.startExternalWriteWatcher();
14399
14748
  logger.info(`Bot ${idx}/${botConfigs.length}: ${cfg.larkAppId} (cli: ${cfg.cliId})`);
14400
14749
  setAskCardDispatcher(createLarkAskCardDispatcher());
@@ -14428,6 +14777,16 @@ export async function startDaemon(botIndex) {
14428
14777
  // briefly sees an unusable on_/email (the resolution below rewrites this field).
14429
14778
  resolvedAllowedUsers: getBot(cfg.larkAppId).resolvedAllowedUsers.filter(u => u.startsWith('ou_')),
14430
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));
14431
14790
  // 名称状态刷新:displayName 或飞书真名变化后,用有效展示名刷新 descriptor +
14432
14791
  // SessionRow.botName,无需重启 daemon。displayName 路径经 bot-config-store 的
14433
14792
  // 钩子触发;真·改名路径在下面的 renamer 里直接调用。
@@ -14677,22 +15036,68 @@ export async function startDaemon(botIndex) {
14677
15036
  // Refresh CLI version per bot's cliId
14678
15037
  refreshCliVersion(cfg.cliId, cfg.cliPathOverride);
14679
15038
  // Resolve allowed users per bot
14680
- if (bot.resolvedAllowedUsers.length > 0) {
15039
+ if ((bot.config.allowedUsers?.length ?? 0) > 0 || bot.resolvedAllowedUsers.length > 0) {
14681
15040
  // 含邮箱或 union_id(on_) 都要重解析成本 app 的 open_id —— 否则 canTalk/canOperate
14682
15041
  // 拿 sender 的 ou_ 对不上 on_,owner 会被自己的 bot 锁死(PR#72)。
14683
15042
  // literal ou_ 也走 best-effort 校验,用于诊断把其他 app 视角 open_id
14684
15043
  // 误填到本 bot 的配置。
14685
- 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_'));
14686
15054
  if (needsResolve) {
15055
+ const previousResolvedMap = readAllowedUsersCache(cfg.larkAppId);
14687
15056
  try {
14688
- // 同时拿到 raw→open_id 映射,供 /revoke 反查删除 email 形式的 raw 条目(R2#2)。
14689
- const { resolved, map } = await resolveAllowedUsersWithMap(cfg.larkAppId, bot.resolvedAllowedUsers);
14690
- bot.resolvedAllowedUsers = resolved;
14691
- bot.rawAllowedUserResolution = map;
14692
- 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
+ }
14693
15078
  }
14694
15079
  catch (err) {
14695
- 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);
14696
15101
  }
14697
15102
  }
14698
15103
  // Republish the descriptor with the post-resolution open_ids so the
@@ -14892,10 +15297,10 @@ export async function startDaemon(botIndex) {
14892
15297
  }, 5_000);
14893
15298
  docCommentPollTimer.unref?.();
14894
15299
  void pollWatchedDocComments(cfg.larkAppId);
14895
- // 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
14896
15301
  // <dataDir>/sandboxes/<sid> whose session is no longer active gets its
14897
- // overlays unmounted and its dirs removed (plus the /var/tmp home scratch).
14898
- // 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.
14899
15304
  try {
14900
15305
  sweepOrphanSandboxes(config.session.dataDir, new Set([...activeSessions.values()].map(ds => ds.session.sessionId)));
14901
15306
  }
@@ -14909,12 +15314,12 @@ export async function startDaemon(botIndex) {
14909
15314
  }, 60_000);
14910
15315
  idleWorkerSweepTimer.unref?.();
14911
15316
  // Periodic sandbox reconciler: the daemon's SIGKILL straggler-reaper (and any
14912
- // worker SIGKILL) bypasses worker-side killCli(), so the overlay mounts +
14913
- // upper/work dirs of a killed-but-still-active sandboxed session would leak for
14914
- // the rest of this daemon's lifetime (one daemon per bot can run for days). The
14915
- // startup sweep alone can't catch a session that dies AFTER boot. This re-runs
14916
- // the sweep on a timer: it reclaims active sessions whose overlays are already
14917
- // 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.
14918
15323
  const sandboxReconcileTimer = setInterval(() => {
14919
15324
  try {
14920
15325
  sweepOrphanSandboxes(config.session.dataDir, new Set([...activeSessions.values()].map(ds => ds.session.sessionId)));
@@ -14984,6 +15389,92 @@ export async function startDaemon(botIndex) {
14984
15389
  });
14985
15390
  }, 5_000).unref?.();
14986
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
+ }
14987
15478
  // Graceful shutdown. Sends SIGTERM (or `{type:'close'}` IPC via killWorker)
14988
15479
  // to every worker, then waits up to SHUTDOWN_GRACE_MS for them to exit
14989
15480
  // before sending SIGKILL to stragglers. Without the wait, daemon