botmux 3.7.1 → 3.8.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 (622) hide show
  1. package/dist/.runtime-build-id +1 -1
  2. package/dist/adapters/backend/capabilities.d.ts +17 -0
  3. package/dist/adapters/backend/capabilities.d.ts.map +1 -0
  4. package/dist/adapters/backend/capabilities.js +22 -0
  5. package/dist/adapters/backend/capabilities.js.map +1 -0
  6. package/dist/adapters/backend/critical-control-key.d.ts +19 -0
  7. package/dist/adapters/backend/critical-control-key.d.ts.map +1 -0
  8. package/dist/adapters/backend/critical-control-key.js +34 -0
  9. package/dist/adapters/backend/critical-control-key.js.map +1 -0
  10. package/dist/adapters/backend/herdr-backend.d.ts +9 -0
  11. package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
  12. package/dist/adapters/backend/herdr-backend.js +23 -4
  13. package/dist/adapters/backend/herdr-backend.js.map +1 -1
  14. package/dist/adapters/backend/riff-backend.d.ts.map +1 -1
  15. package/dist/adapters/backend/riff-backend.js +3 -2
  16. package/dist/adapters/backend/riff-backend.js.map +1 -1
  17. package/dist/adapters/backend/session-backend-selector.d.ts +61 -6
  18. package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -1
  19. package/dist/adapters/backend/session-backend-selector.js +173 -9
  20. package/dist/adapters/backend/session-backend-selector.js.map +1 -1
  21. package/dist/adapters/backend/tmux-backend.d.ts +10 -3
  22. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  23. package/dist/adapters/backend/tmux-backend.js +25 -8
  24. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  25. package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
  26. package/dist/adapters/backend/tmux-pipe-backend.js +3 -2
  27. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
  28. package/dist/adapters/backend/types.d.ts +45 -4
  29. package/dist/adapters/backend/types.d.ts.map +1 -1
  30. package/dist/adapters/backend/types.js +11 -0
  31. package/dist/adapters/backend/types.js.map +1 -1
  32. package/dist/adapters/backend/zellij-backend.d.ts.map +1 -1
  33. package/dist/adapters/backend/zellij-backend.js +3 -2
  34. package/dist/adapters/backend/zellij-backend.js.map +1 -1
  35. package/dist/adapters/backend/zmx-backend.d.ts +286 -0
  36. package/dist/adapters/backend/zmx-backend.d.ts.map +1 -0
  37. package/dist/adapters/backend/zmx-backend.js +2396 -0
  38. package/dist/adapters/backend/zmx-backend.js.map +1 -0
  39. package/dist/adapters/cli/claude-code.d.ts.map +1 -1
  40. package/dist/adapters/cli/claude-code.js +3 -0
  41. package/dist/adapters/cli/claude-code.js.map +1 -1
  42. package/dist/adapters/cli/codex-app.d.ts.map +1 -1
  43. package/dist/adapters/cli/codex-app.js +5 -1
  44. package/dist/adapters/cli/codex-app.js.map +1 -1
  45. package/dist/adapters/cli/codex.d.ts.map +1 -1
  46. package/dist/adapters/cli/codex.js +26 -1
  47. package/dist/adapters/cli/codex.js.map +1 -1
  48. package/dist/adapters/cli/fs-policy.d.ts +79 -1
  49. package/dist/adapters/cli/fs-policy.d.ts.map +1 -1
  50. package/dist/adapters/cli/fs-policy.js +290 -13
  51. package/dist/adapters/cli/fs-policy.js.map +1 -1
  52. package/dist/adapters/cli/grok.d.ts.map +1 -1
  53. package/dist/adapters/cli/grok.js +4 -1
  54. package/dist/adapters/cli/grok.js.map +1 -1
  55. package/dist/adapters/cli/oh-my-pi.d.ts.map +1 -1
  56. package/dist/adapters/cli/oh-my-pi.js +5 -3
  57. package/dist/adapters/cli/oh-my-pi.js.map +1 -1
  58. package/dist/adapters/cli/shared-hints.d.ts +7 -4
  59. package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
  60. package/dist/adapters/cli/shared-hints.js +52 -35
  61. package/dist/adapters/cli/shared-hints.js.map +1 -1
  62. package/dist/adapters/cli/strict-input-handle.d.ts +13 -0
  63. package/dist/adapters/cli/strict-input-handle.d.ts.map +1 -0
  64. package/dist/adapters/cli/strict-input-handle.js +39 -0
  65. package/dist/adapters/cli/strict-input-handle.js.map +1 -0
  66. package/dist/adapters/cli/traex.d.ts.map +1 -1
  67. package/dist/adapters/cli/traex.js +6 -4
  68. package/dist/adapters/cli/traex.js.map +1 -1
  69. package/dist/adapters/cli/types.d.ts +16 -0
  70. package/dist/adapters/cli/types.d.ts.map +1 -1
  71. package/dist/bot-registry.d.ts +137 -8
  72. package/dist/bot-registry.d.ts.map +1 -1
  73. package/dist/bot-registry.js +298 -12
  74. package/dist/bot-registry.js.map +1 -1
  75. package/dist/cli/session-list-liveness.d.ts +1 -1
  76. package/dist/cli/session-list-liveness.js +1 -1
  77. package/dist/cli/zmx-managed-attach.d.ts +41 -0
  78. package/dist/cli/zmx-managed-attach.d.ts.map +1 -0
  79. package/dist/cli/zmx-managed-attach.js +72 -0
  80. package/dist/cli/zmx-managed-attach.js.map +1 -0
  81. package/dist/cli.d.ts.map +1 -1
  82. package/dist/cli.js +780 -131
  83. package/dist/cli.js.map +1 -1
  84. package/dist/codex-app-runner.js +90 -2
  85. package/dist/codex-app-runner.js.map +1 -1
  86. package/dist/codex-rpc-engine.d.ts.map +1 -1
  87. package/dist/codex-rpc-engine.js +22 -6
  88. package/dist/codex-rpc-engine.js.map +1 -1
  89. package/dist/config.d.ts +5 -0
  90. package/dist/config.d.ts.map +1 -1
  91. package/dist/config.js +29 -1
  92. package/dist/config.js.map +1 -1
  93. package/dist/core/ask-broker.d.ts +22 -1
  94. package/dist/core/ask-broker.d.ts.map +1 -1
  95. package/dist/core/ask-broker.js +12 -5
  96. package/dist/core/ask-broker.js.map +1 -1
  97. package/dist/core/botmux-wrapper.d.ts +18 -0
  98. package/dist/core/botmux-wrapper.d.ts.map +1 -1
  99. package/dist/core/botmux-wrapper.js +25 -1
  100. package/dist/core/botmux-wrapper.js.map +1 -1
  101. package/dist/core/chat-rename.d.ts +27 -0
  102. package/dist/core/chat-rename.d.ts.map +1 -0
  103. package/dist/core/chat-rename.js +53 -0
  104. package/dist/core/chat-rename.js.map +1 -0
  105. package/dist/core/command-handler.d.ts.map +1 -1
  106. package/dist/core/command-handler.js +160 -12
  107. package/dist/core/command-handler.js.map +1 -1
  108. package/dist/core/cost-calculator.d.ts +25 -1
  109. package/dist/core/cost-calculator.d.ts.map +1 -1
  110. package/dist/core/cost-calculator.js +102 -4
  111. package/dist/core/cost-calculator.js.map +1 -1
  112. package/dist/core/dashboard-ipc-server.d.ts +16 -2
  113. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  114. package/dist/core/dashboard-ipc-server.js +611 -62
  115. package/dist/core/dashboard-ipc-server.js.map +1 -1
  116. package/dist/core/dashboard-rows.d.ts +22 -1
  117. package/dist/core/dashboard-rows.d.ts.map +1 -1
  118. package/dist/core/dashboard-rows.js +61 -0
  119. package/dist/core/dashboard-rows.js.map +1 -1
  120. package/dist/core/dashboard-url.d.ts +6 -0
  121. package/dist/core/dashboard-url.d.ts.map +1 -1
  122. package/dist/core/dashboard-url.js +11 -1
  123. package/dist/core/dashboard-url.js.map +1 -1
  124. package/dist/core/device-isolation-daemon.d.ts +28 -6
  125. package/dist/core/device-isolation-daemon.d.ts.map +1 -1
  126. package/dist/core/device-isolation-daemon.js +96 -13
  127. package/dist/core/device-isolation-daemon.js.map +1 -1
  128. package/dist/core/host-overload-alert.d.ts +75 -0
  129. package/dist/core/host-overload-alert.d.ts.map +1 -1
  130. package/dist/core/host-overload-alert.js +103 -0
  131. package/dist/core/host-overload-alert.js.map +1 -1
  132. package/dist/core/idle-worker-sweeper.js +1 -1
  133. package/dist/core/idle-worker-sweeper.js.map +1 -1
  134. package/dist/core/inflight-input-tracker.d.ts +3 -0
  135. package/dist/core/inflight-input-tracker.d.ts.map +1 -1
  136. package/dist/core/inflight-input-tracker.js +9 -0
  137. package/dist/core/inflight-input-tracker.js.map +1 -1
  138. package/dist/core/passthrough-commands.d.ts.map +1 -1
  139. package/dist/core/passthrough-commands.js +10 -0
  140. package/dist/core/passthrough-commands.js.map +1 -1
  141. package/dist/core/persistent-backend.d.ts +32 -4
  142. package/dist/core/persistent-backend.d.ts.map +1 -1
  143. package/dist/core/persistent-backend.js +75 -6
  144. package/dist/core/persistent-backend.js.map +1 -1
  145. package/dist/core/plugins/mcp/gateway.js +2 -2
  146. package/dist/core/plugins/mcp/gateway.js.map +1 -1
  147. package/dist/core/plugins/runtime.d.ts +3 -0
  148. package/dist/core/plugins/runtime.d.ts.map +1 -1
  149. package/dist/core/plugins/runtime.js.map +1 -1
  150. package/dist/core/plugins/service-manager.d.ts +4 -1
  151. package/dist/core/plugins/service-manager.d.ts.map +1 -1
  152. package/dist/core/plugins/service-manager.js +11 -6
  153. package/dist/core/plugins/service-manager.js.map +1 -1
  154. package/dist/core/role-library.d.ts +48 -1
  155. package/dist/core/role-library.d.ts.map +1 -1
  156. package/dist/core/role-library.js +111 -3
  157. package/dist/core/role-library.js.map +1 -1
  158. package/dist/core/role-resolver.d.ts +14 -2
  159. package/dist/core/role-resolver.d.ts.map +1 -1
  160. package/dist/core/role-resolver.js +47 -4
  161. package/dist/core/role-resolver.js.map +1 -1
  162. package/dist/core/session-activity.d.ts +14 -0
  163. package/dist/core/session-activity.d.ts.map +1 -1
  164. package/dist/core/session-activity.js +46 -1
  165. package/dist/core/session-activity.js.map +1 -1
  166. package/dist/core/session-board.d.ts +5 -2
  167. package/dist/core/session-board.d.ts.map +1 -1
  168. package/dist/core/session-board.js +5 -2
  169. package/dist/core/session-board.js.map +1 -1
  170. package/dist/core/session-liveness.d.ts +6 -5
  171. package/dist/core/session-liveness.d.ts.map +1 -1
  172. package/dist/core/session-liveness.js +34 -13
  173. package/dist/core/session-liveness.js.map +1 -1
  174. package/dist/core/session-manager.d.ts +3 -2
  175. package/dist/core/session-manager.d.ts.map +1 -1
  176. package/dist/core/session-manager.js +397 -103
  177. package/dist/core/session-manager.js.map +1 -1
  178. package/dist/core/session-marker.d.ts +11 -2
  179. package/dist/core/session-marker.d.ts.map +1 -1
  180. package/dist/core/session-marker.js +50 -11
  181. package/dist/core/session-marker.js.map +1 -1
  182. package/dist/core/session-message-preview.d.ts +20 -0
  183. package/dist/core/session-message-preview.d.ts.map +1 -0
  184. package/dist/core/session-message-preview.js +188 -0
  185. package/dist/core/session-message-preview.js.map +1 -0
  186. package/dist/core/session-title.d.ts +5 -1
  187. package/dist/core/session-title.d.ts.map +1 -1
  188. package/dist/core/session-title.js +20 -3
  189. package/dist/core/session-title.js.map +1 -1
  190. package/dist/core/startup-commands.d.ts +1 -1
  191. package/dist/core/startup-commands.js +1 -1
  192. package/dist/core/terminal-url.d.ts.map +1 -1
  193. package/dist/core/terminal-url.js +3 -2
  194. package/dist/core/terminal-url.js.map +1 -1
  195. package/dist/core/trigger-session.d.ts.map +1 -1
  196. package/dist/core/trigger-session.js +75 -12
  197. package/dist/core/trigger-session.js.map +1 -1
  198. package/dist/core/types.d.ts +35 -0
  199. package/dist/core/types.d.ts.map +1 -1
  200. package/dist/core/types.js +23 -0
  201. package/dist/core/types.js.map +1 -1
  202. package/dist/core/worker-ipc.d.ts +11 -0
  203. package/dist/core/worker-ipc.d.ts.map +1 -0
  204. package/dist/core/worker-ipc.js +26 -0
  205. package/dist/core/worker-ipc.js.map +1 -0
  206. package/dist/core/worker-pool.d.ts +179 -29
  207. package/dist/core/worker-pool.d.ts.map +1 -1
  208. package/dist/core/worker-pool.js +2118 -331
  209. package/dist/core/worker-pool.js.map +1 -1
  210. package/dist/daemon.d.ts +132 -3
  211. package/dist/daemon.d.ts.map +1 -1
  212. package/dist/daemon.js +1327 -449
  213. package/dist/daemon.js.map +1 -1
  214. package/dist/dashboard/auth.d.ts +2 -0
  215. package/dist/dashboard/auth.d.ts.map +1 -1
  216. package/dist/dashboard/auth.js +9 -0
  217. package/dist/dashboard/auth.js.map +1 -1
  218. package/dist/dashboard/bot-payload.d.ts +4 -0
  219. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  220. package/dist/dashboard/bot-payload.js +5 -0
  221. package/dist/dashboard/bot-payload.js.map +1 -1
  222. package/dist/dashboard/federation-api.d.ts.map +1 -1
  223. package/dist/dashboard/federation-api.js +4 -0
  224. package/dist/dashboard/federation-api.js.map +1 -1
  225. package/dist/dashboard/federation-spoke-api.d.ts.map +1 -1
  226. package/dist/dashboard/federation-spoke-api.js +32 -5
  227. package/dist/dashboard/federation-spoke-api.js.map +1 -1
  228. package/dist/dashboard/public-redact.d.ts +8 -2
  229. package/dist/dashboard/public-redact.d.ts.map +1 -1
  230. package/dist/dashboard/public-redact.js +34 -7
  231. package/dist/dashboard/public-redact.js.map +1 -1
  232. package/dist/dashboard/session-card-model.d.ts.map +1 -1
  233. package/dist/dashboard/session-card-model.js +10 -2
  234. package/dist/dashboard/session-card-model.js.map +1 -1
  235. package/dist/dashboard/settings-write-applier.d.ts +35 -1
  236. package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
  237. package/dist/dashboard/settings-write-applier.js +88 -1
  238. package/dist/dashboard/settings-write-applier.js.map +1 -1
  239. package/dist/dashboard/web/app.d.ts.map +1 -1
  240. package/dist/dashboard/web/app.js +0 -1
  241. package/dist/dashboard/web/app.js.map +1 -1
  242. package/dist/dashboard/web/bot-defaults-page.d.ts +15 -0
  243. package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
  244. package/dist/dashboard/web/bot-defaults-page.js +212 -58
  245. package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
  246. package/dist/dashboard/web/bot-defaults.d.ts +3 -0
  247. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  248. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  249. package/dist/dashboard/web/bot-onboarding.d.ts +2 -1
  250. package/dist/dashboard/web/bot-onboarding.d.ts.map +1 -1
  251. package/dist/dashboard/web/bot-onboarding.js.map +1 -1
  252. package/dist/dashboard/web/clipboard.d.ts +2 -0
  253. package/dist/dashboard/web/clipboard.d.ts.map +1 -0
  254. package/dist/dashboard/web/clipboard.js +42 -0
  255. package/dist/dashboard/web/clipboard.js.map +1 -0
  256. package/dist/dashboard/web/connectors-page.d.ts.map +1 -1
  257. package/dist/dashboard/web/connectors-page.js +12 -8
  258. package/dist/dashboard/web/connectors-page.js.map +1 -1
  259. package/dist/dashboard/web/dashboard-components.d.ts +17 -16
  260. package/dist/dashboard/web/dashboard-components.d.ts.map +1 -1
  261. package/dist/dashboard/web/dashboard-components.js.map +1 -1
  262. package/dist/dashboard/web/groups-page.d.ts.map +1 -1
  263. package/dist/dashboard/web/groups-page.js +7 -3
  264. package/dist/dashboard/web/groups-page.js.map +1 -1
  265. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  266. package/dist/dashboard/web/i18n.js +205 -12
  267. package/dist/dashboard/web/i18n.js.map +1 -1
  268. package/dist/dashboard/web/icon-tooltip.d.ts.map +1 -1
  269. package/dist/dashboard/web/icon-tooltip.js +16 -1
  270. package/dist/dashboard/web/icon-tooltip.js.map +1 -1
  271. package/dist/dashboard/web/listener-filters.d.ts +23 -0
  272. package/dist/dashboard/web/listener-filters.d.ts.map +1 -0
  273. package/dist/dashboard/web/listener-filters.js +36 -0
  274. package/dist/dashboard/web/listener-filters.js.map +1 -0
  275. package/dist/dashboard/web/overview-page.d.ts.map +1 -1
  276. package/dist/dashboard/web/overview-page.js.map +1 -1
  277. package/dist/dashboard/web/plugin-page.d.ts.map +1 -1
  278. package/dist/dashboard/web/plugin-page.js.map +1 -1
  279. package/dist/dashboard/web/preview-markdown.d.ts +5 -0
  280. package/dist/dashboard/web/preview-markdown.d.ts.map +1 -0
  281. package/dist/dashboard/web/preview-markdown.js +52 -0
  282. package/dist/dashboard/web/preview-markdown.js.map +1 -0
  283. package/dist/dashboard/web/roles-page.d.ts.map +1 -1
  284. package/dist/dashboard/web/roles-page.js +535 -10
  285. package/dist/dashboard/web/roles-page.js.map +1 -1
  286. package/dist/dashboard/web/roles.d.ts +77 -0
  287. package/dist/dashboard/web/roles.d.ts.map +1 -1
  288. package/dist/dashboard/web/roles.js +98 -0
  289. package/dist/dashboard/web/roles.js.map +1 -1
  290. package/dist/dashboard/web/session-terminal.d.ts.map +1 -1
  291. package/dist/dashboard/web/session-terminal.js +6 -4
  292. package/dist/dashboard/web/session-terminal.js.map +1 -1
  293. package/dist/dashboard/web/sessions-kanban.d.ts +4 -1
  294. package/dist/dashboard/web/sessions-kanban.d.ts.map +1 -1
  295. package/dist/dashboard/web/sessions-kanban.js +1 -1
  296. package/dist/dashboard/web/sessions-kanban.js.map +1 -1
  297. package/dist/dashboard/web/sessions-page.d.ts +3 -2
  298. package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
  299. package/dist/dashboard/web/sessions-page.js +163 -28
  300. package/dist/dashboard/web/sessions-page.js.map +1 -1
  301. package/dist/dashboard/web/sessions.d.ts +25 -0
  302. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  303. package/dist/dashboard/web/sessions.js +58 -7
  304. package/dist/dashboard/web/sessions.js.map +1 -1
  305. package/dist/dashboard/web/settings-page.d.ts +28 -3
  306. package/dist/dashboard/web/settings-page.d.ts.map +1 -1
  307. package/dist/dashboard/web/settings-page.js +61 -1
  308. package/dist/dashboard/web/settings-page.js.map +1 -1
  309. package/dist/dashboard/web/skills-page.d.ts +6 -5
  310. package/dist/dashboard/web/skills-page.d.ts.map +1 -1
  311. package/dist/dashboard/web/skills-page.js +2 -0
  312. package/dist/dashboard/web/skills-page.js.map +1 -1
  313. package/dist/dashboard/web/v3-components.d.ts +3 -2
  314. package/dist/dashboard/web/v3-components.d.ts.map +1 -1
  315. package/dist/dashboard/web/v3-components.js.map +1 -1
  316. package/dist/dashboard/web/vc-consumer-profiles-section.d.ts +12 -2
  317. package/dist/dashboard/web/vc-consumer-profiles-section.d.ts.map +1 -1
  318. package/dist/dashboard/web/vc-consumer-profiles-section.js +0 -9
  319. package/dist/dashboard/web/vc-consumer-profiles-section.js.map +1 -1
  320. package/dist/dashboard/web/webhook-logs-page.d.ts +2 -1
  321. package/dist/dashboard/web/webhook-logs-page.d.ts.map +1 -1
  322. package/dist/dashboard/web/webhook-logs-page.js +4 -1
  323. package/dist/dashboard/web/webhook-logs-page.js.map +1 -1
  324. package/dist/dashboard-web/app.js +1 -1
  325. package/dist/dashboard-web/chunks/bot-defaults-page-FAPIMQV3.js +10 -0
  326. package/dist/dashboard-web/chunks/{chunk-QFMH6ITB.js → chunk-5CJMHTT4.js} +1 -1
  327. package/dist/dashboard-web/chunks/{chunk-KQB42ML5.js → chunk-DLZK52JU.js} +14 -12
  328. package/dist/dashboard-web/chunks/chunk-FDZ7WOBW.js +16 -0
  329. package/dist/dashboard-web/chunks/{chunk-3QAUJXAS.js → chunk-GTAJV5YC.js} +1 -1
  330. package/dist/dashboard-web/chunks/{chunk-FNPGOZMX.js → chunk-IHNG6KE6.js} +1 -1
  331. package/dist/dashboard-web/chunks/{chunk-FLGXZJTD.js → chunk-VA6QWSGQ.js} +1 -1
  332. package/dist/dashboard-web/chunks/chunk-XJ7BGPJH.js +1 -0
  333. package/dist/dashboard-web/chunks/chunk-ZJVTGU5X.js +1 -0
  334. package/dist/dashboard-web/chunks/connectors-page-LKWRT3GS.js +1 -0
  335. package/dist/dashboard-web/chunks/groups-page-V6654ICQ.js +8 -0
  336. package/dist/dashboard-web/chunks/insights-page-237RMFQ5.js +7 -0
  337. package/dist/dashboard-web/chunks/{monitor-room-7IVIS75W.js → monitor-room-OO5Y4FRQ.js} +1 -1
  338. package/dist/dashboard-web/chunks/{monitoring-page-5SSJRQZF.js → monitoring-page-OUEITM5P.js} +1 -1
  339. package/dist/dashboard-web/chunks/{office-page-EOND3L5D.js → office-page-4KNAHQ4Q.js} +1 -1
  340. package/dist/dashboard-web/chunks/{overview-page-NISBVBBJ.js → overview-page-QDP4YK2S.js} +1 -1
  341. package/dist/dashboard-web/chunks/{plugin-page-JXC5F5YV.js → plugin-page-VEGTCEAS.js} +1 -1
  342. package/dist/dashboard-web/chunks/roles-page-PGXNZZNP.js +1 -0
  343. package/dist/dashboard-web/chunks/{schedules-page-AVOOKLUH.js → schedules-page-BJNC5TID.js} +1 -1
  344. package/dist/dashboard-web/chunks/sessions-page-M2ZYYCMO.js +6 -0
  345. package/dist/dashboard-web/chunks/settings-page-IJFZKUUW.js +2 -0
  346. package/dist/dashboard-web/chunks/{skills-page-5MW54PCC.js → skills-page-YRAIRZOQ.js} +3 -3
  347. package/dist/dashboard-web/chunks/{team-federation-page-2SNIMVXI.js → team-federation-page-DEU27GRU.js} +1 -1
  348. package/dist/dashboard-web/chunks/{v3-page-W6SBXXQC.js → v3-page-6KHEW2YL.js} +5 -5
  349. package/dist/dashboard-web/chunks/{whiteboards-page-STC54KTP.js → whiteboards-page-VDECAALH.js} +1 -1
  350. package/dist/dashboard-web/style.css +12545 -11432
  351. package/dist/dashboard.js +215 -36
  352. package/dist/dashboard.js.map +1 -1
  353. package/dist/global-config.d.ts +41 -2
  354. package/dist/global-config.d.ts.map +1 -1
  355. package/dist/global-config.js +47 -0
  356. package/dist/global-config.js.map +1 -1
  357. package/dist/i18n/en.d.ts.map +1 -1
  358. package/dist/i18n/en.js +71 -19
  359. package/dist/i18n/en.js.map +1 -1
  360. package/dist/i18n/zh.d.ts.map +1 -1
  361. package/dist/i18n/zh.js +71 -19
  362. package/dist/i18n/zh.js.map +1 -1
  363. package/dist/im/lark/callback-button-marker.d.ts +34 -0
  364. package/dist/im/lark/callback-button-marker.d.ts.map +1 -0
  365. package/dist/im/lark/callback-button-marker.js +99 -0
  366. package/dist/im/lark/callback-button-marker.js.map +1 -0
  367. package/dist/im/lark/card-builder.d.ts +46 -10
  368. package/dist/im/lark/card-builder.d.ts.map +1 -1
  369. package/dist/im/lark/card-builder.js +299 -78
  370. package/dist/im/lark/card-builder.js.map +1 -1
  371. package/dist/im/lark/card-handler.d.ts +3 -4
  372. package/dist/im/lark/card-handler.d.ts.map +1 -1
  373. package/dist/im/lark/card-handler.js +380 -148
  374. package/dist/im/lark/card-handler.js.map +1 -1
  375. package/dist/im/lark/client.d.ts +24 -1
  376. package/dist/im/lark/client.d.ts.map +1 -1
  377. package/dist/im/lark/client.js +101 -8
  378. package/dist/im/lark/client.js.map +1 -1
  379. package/dist/im/lark/doc-comment.d.ts.map +1 -1
  380. package/dist/im/lark/doc-comment.js +6 -1
  381. package/dist/im/lark/doc-comment.js.map +1 -1
  382. package/dist/im/lark/event-dispatcher.d.ts +115 -3
  383. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  384. package/dist/im/lark/event-dispatcher.js +534 -147
  385. package/dist/im/lark/event-dispatcher.js.map +1 -1
  386. package/dist/im/lark/grant-command.d.ts +16 -12
  387. package/dist/im/lark/grant-command.d.ts.map +1 -1
  388. package/dist/im/lark/grant-command.js +48 -151
  389. package/dist/im/lark/grant-command.js.map +1 -1
  390. package/dist/im/lark/grant-pending.d.ts +12 -4
  391. package/dist/im/lark/grant-pending.d.ts.map +1 -1
  392. package/dist/im/lark/grant-pending.js +31 -8
  393. package/dist/im/lark/grant-pending.js.map +1 -1
  394. package/dist/im/lark/invite-command.d.ts +20 -0
  395. package/dist/im/lark/invite-command.d.ts.map +1 -0
  396. package/dist/im/lark/invite-command.js +239 -0
  397. package/dist/im/lark/invite-command.js.map +1 -0
  398. package/dist/im/lark/md-card.d.ts +66 -4
  399. package/dist/im/lark/md-card.d.ts.map +1 -1
  400. package/dist/im/lark/md-card.js +201 -31
  401. package/dist/im/lark/md-card.js.map +1 -1
  402. package/dist/im/lark/mention-targets.d.ts +43 -0
  403. package/dist/im/lark/mention-targets.d.ts.map +1 -0
  404. package/dist/im/lark/mention-targets.js +190 -0
  405. package/dist/im/lark/mention-targets.js.map +1 -0
  406. package/dist/im/lark/message-parser.d.ts +31 -0
  407. package/dist/im/lark/message-parser.d.ts.map +1 -1
  408. package/dist/im/lark/message-parser.js +305 -32
  409. package/dist/im/lark/message-parser.js.map +1 -1
  410. package/dist/im/lark/reply-card-footer-signature.d.ts +7 -0
  411. package/dist/im/lark/reply-card-footer-signature.d.ts.map +1 -0
  412. package/dist/im/lark/reply-card-footer-signature.js +7 -0
  413. package/dist/im/lark/reply-card-footer-signature.js.map +1 -0
  414. package/dist/im/lark/sessions-card.d.ts.map +1 -1
  415. package/dist/im/lark/sessions-card.js +4 -1
  416. package/dist/im/lark/sessions-card.js.map +1 -1
  417. package/dist/im/lark/substitute-command.d.ts.map +1 -1
  418. package/dist/im/lark/substitute-command.js +8 -0
  419. package/dist/im/lark/substitute-command.js.map +1 -1
  420. package/dist/im/lark/topic-root-context.d.ts +35 -0
  421. package/dist/im/lark/topic-root-context.d.ts.map +1 -0
  422. package/dist/im/lark/topic-root-context.js +37 -0
  423. package/dist/im/lark/topic-root-context.js.map +1 -0
  424. package/dist/im/lark/v3-blocked-card.d.ts.map +1 -1
  425. package/dist/im/lark/v3-blocked-card.js +2 -1
  426. package/dist/im/lark/v3-blocked-card.js.map +1 -1
  427. package/dist/im/lark/v3-gate-card.d.ts.map +1 -1
  428. package/dist/im/lark/v3-gate-card.js +2 -1
  429. package/dist/im/lark/v3-gate-card.js.map +1 -1
  430. package/dist/im/lark/v3-loop-grant-card.d.ts.map +1 -1
  431. package/dist/im/lark/v3-loop-grant-card.js +2 -1
  432. package/dist/im/lark/v3-loop-grant-card.js.map +1 -1
  433. package/dist/im/lark/v3-progress-card.d.ts.map +1 -1
  434. package/dist/im/lark/v3-progress-card.js +2 -1
  435. package/dist/im/lark/v3-progress-card.js.map +1 -1
  436. package/dist/im/lark/v3-revisit-grant-card.d.ts.map +1 -1
  437. package/dist/im/lark/v3-revisit-grant-card.js +2 -1
  438. package/dist/im/lark/v3-revisit-grant-card.js.map +1 -1
  439. package/dist/index-core-only.d.ts +3 -0
  440. package/dist/index-core-only.d.ts.map +1 -0
  441. package/dist/index-core-only.js +107 -0
  442. package/dist/index-core-only.js.map +1 -0
  443. package/dist/services/async-trigger-state.d.ts +8 -0
  444. package/dist/services/async-trigger-state.d.ts.map +1 -1
  445. package/dist/services/async-trigger-state.js +3 -2
  446. package/dist/services/async-trigger-state.js.map +1 -1
  447. package/dist/services/async-trigger-store.d.ts +10 -2
  448. package/dist/services/async-trigger-store.d.ts.map +1 -1
  449. package/dist/services/async-trigger-store.js +3 -2
  450. package/dist/services/async-trigger-store.js.map +1 -1
  451. package/dist/services/backend-availability.d.ts +3 -1
  452. package/dist/services/backend-availability.d.ts.map +1 -1
  453. package/dist/services/backend-availability.js +14 -1
  454. package/dist/services/backend-availability.js.map +1 -1
  455. package/dist/services/backend-type-store.d.ts.map +1 -1
  456. package/dist/services/backend-type-store.js +1 -1
  457. package/dist/services/backend-type-store.js.map +1 -1
  458. package/dist/services/bot-config-store.d.ts.map +1 -1
  459. package/dist/services/bot-config-store.js +2 -1
  460. package/dist/services/bot-config-store.js.map +1 -1
  461. package/dist/services/bridge-fallback-gate.d.ts +14 -1
  462. package/dist/services/bridge-fallback-gate.d.ts.map +1 -1
  463. package/dist/services/bridge-fallback-gate.js +65 -1
  464. package/dist/services/bridge-fallback-gate.js.map +1 -1
  465. package/dist/services/card-prefs-store.d.ts +7 -2
  466. package/dist/services/card-prefs-store.d.ts.map +1 -1
  467. package/dist/services/card-prefs-store.js +30 -4
  468. package/dist/services/card-prefs-store.js.map +1 -1
  469. package/dist/services/codex-app-runner-protocol.d.ts +9 -0
  470. package/dist/services/codex-app-runner-protocol.d.ts.map +1 -1
  471. package/dist/services/codex-app-runner-protocol.js +17 -0
  472. package/dist/services/codex-app-runner-protocol.js.map +1 -1
  473. package/dist/services/codex-app-token-usage.d.ts +93 -0
  474. package/dist/services/codex-app-token-usage.d.ts.map +1 -0
  475. package/dist/services/codex-app-token-usage.js +190 -0
  476. package/dist/services/codex-app-token-usage.js.map +1 -0
  477. package/dist/services/codex-transcript.d.ts +6 -1
  478. package/dist/services/codex-transcript.d.ts.map +1 -1
  479. package/dist/services/codex-transcript.js +103 -32
  480. package/dist/services/codex-transcript.js.map +1 -1
  481. package/dist/services/federation-roster.d.ts +4 -0
  482. package/dist/services/federation-roster.d.ts.map +1 -1
  483. package/dist/services/federation-roster.js +3 -0
  484. package/dist/services/federation-roster.js.map +1 -1
  485. package/dist/services/federation-store.d.ts +6 -0
  486. package/dist/services/federation-store.d.ts.map +1 -1
  487. package/dist/services/federation-store.js.map +1 -1
  488. package/dist/services/grant-policy.d.ts +13 -0
  489. package/dist/services/grant-policy.d.ts.map +1 -0
  490. package/dist/services/grant-policy.js +40 -0
  491. package/dist/services/grant-policy.js.map +1 -0
  492. package/dist/services/grant-store.d.ts +24 -3
  493. package/dist/services/grant-store.d.ts.map +1 -1
  494. package/dist/services/grant-store.js +196 -12
  495. package/dist/services/grant-store.js.map +1 -1
  496. package/dist/services/groups-store.d.ts +36 -0
  497. package/dist/services/groups-store.d.ts.map +1 -1
  498. package/dist/services/groups-store.js +123 -1
  499. package/dist/services/groups-store.js.map +1 -1
  500. package/dist/services/jsonl-cursor.d.ts.map +1 -1
  501. package/dist/services/jsonl-cursor.js +8 -2
  502. package/dist/services/jsonl-cursor.js.map +1 -1
  503. package/dist/services/local-cli-opener.d.ts.map +1 -1
  504. package/dist/services/local-cli-opener.js +32 -0
  505. package/dist/services/local-cli-opener.js.map +1 -1
  506. package/dist/services/message-listener-run-preview-store.d.ts +43 -0
  507. package/dist/services/message-listener-run-preview-store.d.ts.map +1 -0
  508. package/dist/services/message-listener-run-preview-store.js +152 -0
  509. package/dist/services/message-listener-run-preview-store.js.map +1 -0
  510. package/dist/services/message-listener-store.d.ts +26 -0
  511. package/dist/services/message-listener-store.d.ts.map +1 -0
  512. package/dist/services/message-listener-store.js +138 -0
  513. package/dist/services/message-listener-store.js.map +1 -0
  514. package/dist/services/message-listener.d.ts +112 -0
  515. package/dist/services/message-listener.d.ts.map +1 -0
  516. package/dist/services/message-listener.js +391 -0
  517. package/dist/services/message-listener.js.map +1 -0
  518. package/dist/services/overload-alert-migration.d.ts +15 -0
  519. package/dist/services/overload-alert-migration.d.ts.map +1 -0
  520. package/dist/services/overload-alert-migration.js +115 -0
  521. package/dist/services/overload-alert-migration.js.map +1 -0
  522. package/dist/services/send-policy.d.ts +38 -0
  523. package/dist/services/send-policy.d.ts.map +1 -1
  524. package/dist/services/send-policy.js +34 -0
  525. package/dist/services/send-policy.js.map +1 -1
  526. package/dist/services/session-store.d.ts +12 -1
  527. package/dist/services/session-store.d.ts.map +1 -1
  528. package/dist/services/session-store.js +44 -2
  529. package/dist/services/session-store.js.map +1 -1
  530. package/dist/services/skill-registry-store.d.ts.map +1 -1
  531. package/dist/services/skill-registry-store.js +87 -12
  532. package/dist/services/skill-registry-store.js.map +1 -1
  533. package/dist/services/substitute-mode-normalize.d.ts +3 -0
  534. package/dist/services/substitute-mode-normalize.d.ts.map +1 -1
  535. package/dist/services/substitute-mode-normalize.js +8 -0
  536. package/dist/services/substitute-mode-normalize.js.map +1 -1
  537. package/dist/services/substitute-mode-store.d.ts.map +1 -1
  538. package/dist/services/substitute-mode-store.js +8 -1
  539. package/dist/services/substitute-mode-store.js.map +1 -1
  540. package/dist/services/transcript-resolver.d.ts +6 -1
  541. package/dist/services/transcript-resolver.d.ts.map +1 -1
  542. package/dist/services/transcript-resolver.js +145 -29
  543. package/dist/services/transcript-resolver.js.map +1 -1
  544. package/dist/services/trigger-types.d.ts +16 -0
  545. package/dist/services/trigger-types.d.ts.map +1 -1
  546. package/dist/services/trigger-types.js +6 -0
  547. package/dist/services/trigger-types.js.map +1 -1
  548. package/dist/setup/bot-config-editor.js +2 -2
  549. package/dist/setup/bot-config-editor.js.map +1 -1
  550. package/dist/setup/ensure-zmx.d.ts +31 -0
  551. package/dist/setup/ensure-zmx.d.ts.map +1 -0
  552. package/dist/setup/ensure-zmx.js +161 -0
  553. package/dist/setup/ensure-zmx.js.map +1 -0
  554. package/dist/setup/index.d.ts +1 -1
  555. package/dist/setup/index.js +1 -1
  556. package/dist/setup/open-platform-automation.d.ts +9 -0
  557. package/dist/setup/open-platform-automation.d.ts.map +1 -1
  558. package/dist/setup/open-platform-automation.js +15 -2
  559. package/dist/setup/open-platform-automation.js.map +1 -1
  560. package/dist/setup/setup-args.d.ts +1 -1
  561. package/dist/setup/setup-args.d.ts.map +1 -1
  562. package/dist/setup/setup-args.js +1 -1
  563. package/dist/skills/definitions.d.ts.map +1 -1
  564. package/dist/skills/definitions.js +32 -0
  565. package/dist/skills/definitions.js.map +1 -1
  566. package/dist/skills/injection-mode.d.ts +6 -2
  567. package/dist/skills/injection-mode.d.ts.map +1 -1
  568. package/dist/skills/injection-mode.js +10 -5
  569. package/dist/skills/injection-mode.js.map +1 -1
  570. package/dist/types.d.ts +79 -3
  571. package/dist/types.d.ts.map +1 -1
  572. package/dist/utils/child-env.d.ts +1 -1
  573. package/dist/utils/child-env.d.ts.map +1 -1
  574. package/dist/utils/child-env.js +4 -0
  575. package/dist/utils/child-env.js.map +1 -1
  576. package/dist/utils/effort-confirm-dialog.d.ts +40 -0
  577. package/dist/utils/effort-confirm-dialog.d.ts.map +1 -0
  578. package/dist/utils/effort-confirm-dialog.js +74 -0
  579. package/dist/utils/effort-confirm-dialog.js.map +1 -0
  580. package/dist/utils/pending-input-queue.d.ts +56 -0
  581. package/dist/utils/pending-input-queue.d.ts.map +1 -1
  582. package/dist/utils/pending-input-queue.js +80 -0
  583. package/dist/utils/pending-input-queue.js.map +1 -1
  584. package/dist/utils/render-dimensions.d.ts +8 -0
  585. package/dist/utils/render-dimensions.d.ts.map +1 -1
  586. package/dist/utils/render-dimensions.js +10 -0
  587. package/dist/utils/render-dimensions.js.map +1 -1
  588. package/dist/utils/stuck-key-guard.d.ts +3 -2
  589. package/dist/utils/stuck-key-guard.d.ts.map +1 -1
  590. package/dist/utils/stuck-key-guard.js +3 -3
  591. package/dist/utils/stuck-key-guard.js.map +1 -1
  592. package/dist/utils/terminal-renderer.d.ts +6 -0
  593. package/dist/utils/terminal-renderer.d.ts.map +1 -1
  594. package/dist/utils/terminal-renderer.js +15 -0
  595. package/dist/utils/terminal-renderer.js.map +1 -1
  596. package/dist/utils/tui-input-delivery.d.ts +32 -0
  597. package/dist/utils/tui-input-delivery.d.ts.map +1 -0
  598. package/dist/utils/tui-input-delivery.js +53 -0
  599. package/dist/utils/tui-input-delivery.js.map +1 -0
  600. package/dist/utils/xml.d.ts +19 -0
  601. package/dist/utils/xml.d.ts.map +1 -0
  602. package/dist/utils/xml.js +26 -0
  603. package/dist/utils/xml.js.map +1 -0
  604. package/dist/worker.js +1852 -374
  605. package/dist/worker.js.map +1 -1
  606. package/dist/workflows/definition.d.ts +8 -8
  607. package/dist/workflows/events/payloads.d.ts +8 -8
  608. package/dist/workflows/events/schema.d.ts +160 -160
  609. package/dist/workflows/v3/session-relay-client.js +1 -1
  610. package/dist/workflows/v3/session-relay-client.js.map +1 -1
  611. package/dist/workflows/v3/worker-fence.d.ts.map +1 -1
  612. package/dist/workflows/v3/worker-fence.js +15 -10
  613. package/dist/workflows/v3/worker-fence.js.map +1 -1
  614. package/package.json +1 -1
  615. package/dist/dashboard-web/chunks/bot-defaults-page-UKA7K4JV.js +0 -10
  616. package/dist/dashboard-web/chunks/chunk-DFLNXQNT.js +0 -1
  617. package/dist/dashboard-web/chunks/connectors-page-A77YAB5Y.js +0 -1
  618. package/dist/dashboard-web/chunks/groups-page-RUZNVXQB.js +0 -8
  619. package/dist/dashboard-web/chunks/insights-page-RF7U7RTA.js +0 -22
  620. package/dist/dashboard-web/chunks/roles-page-BUMKD7Y4.js +0 -1
  621. package/dist/dashboard-web/chunks/sessions-page-6HLSEAB7.js +0 -2
  622. package/dist/dashboard-web/chunks/settings-page-MJDZHQ4X.js +0 -2
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * botmux status — show daemon status
14
14
  * botmux upgrade|update — upgrade to latest version
15
15
  * botmux device enroll|status|logout — manage the host desktop device credential
16
- * botmux list — interactive session picker (TUI), attach to tmux
16
+ * botmux list — interactive session picker (TUI), attach to managed tmux/ZMX sessions
17
17
  * botmux list --plain — plain table output (for piping / scripts)
18
18
  * botmux delete <id> — close a session by ID prefix
19
19
  * botmux delete all — close all active sessions
@@ -53,6 +53,7 @@ import { scheduleTimeZone } from './utils/timezone.js';
53
53
  import { expandHomePath, invalidWorkingDirs } from './utils/working-dir.js';
54
54
  import { firstPositional } from './cli/arg-utils.js';
55
55
  import { isColdResumeDormant, sessionListDisposition } from './cli/session-list-liveness.js';
56
+ import { attachFrozenManagedZmxSession, freezeManagedZmxAttachTarget, } from './cli/zmx-managed-attach.js';
56
57
  import { dispatchPrimaryMessage, findStdinAliasAttachment, normalizeInteractiveCardInput, sendFileAttachments, sendVideoAttachments, shouldSendAsPureVideo, validateVideoAttachments } from './cli/send-dispatch.js';
57
58
  import { dispatchDeferredTopicSend } from './cli/deferred-topic-send.js';
58
59
  import { resolveDaemonEnv } from './cli/daemon-lifecycle-env.js';
@@ -70,11 +71,11 @@ import { rejectLikelyWindowsStdinMojibake, decodeStdinBytes } from './cli/stdin-
70
71
  import { formatBotInfoEntriesForCli, formatChatBotsForCli, } from './cli/bots-list-output.js';
71
72
  import { ensureBotChatGrantMatrix, requestExactChatGrant } from './cli/exact-chat-grant-client.js';
72
73
  import { buildFooterAddressing, hasKnownBotMention, knownBotOpenIdsFromCrossRef, orderedFooterRecipients, stripCodeSpans, } from './utils/bot-routing.js';
73
- import { isLocale, localeForBot, setDefaultLocale, SUPPORTED_LOCALES, t } from './i18n/index.js';
74
+ import { isLocale, localeForBot, setDefaultLocale, SUPPORTED_LOCALES } from './i18n/index.js';
74
75
  import { chatAppLink, larkHosts, normalizeBrand, sdkDomain } from './im/lark/lark-hosts.js';
75
76
  import { mergeDashboardConfig, mergeGlobalConfig, readGlobalConfig, setGlobalLocale, globalConfigPath } from './global-config.js';
76
77
  import { createWhiteboard, ensureDefaultWhiteboard, getWhiteboard, listWhiteboards, readWhiteboard, whiteboardEnabled, whiteboardPath, } from './services/whiteboard-store.js';
77
- import { buildBridgeSendMarkerContent } from './services/bridge-fallback-gate.js';
78
+ import { buildBridgeSendMarkerContent, buildBridgeSendPreviewText, } from './services/bridge-fallback-gate.js';
78
79
  import { bindRestartLeaseTo, writeManualIntentIfAbsentTo } from './services/restart-intent-store.js';
79
80
  import { repairMissingChatScope, stripLegacyPendingCardFields } from './services/session-store.js';
80
81
  import { evaluateVcMeetingManagedSend, isTrustedVcMeetingHostRelayParent, resolveVcMeetingImTurnOrigin, } from './services/vc-meeting-send-policy.js';
@@ -86,6 +87,7 @@ import { assertPluginBindingTransition, describePluginDependencyError, enabledPl
86
87
  import { authorizeV3DaemonCommand } from './workflows/v3/cli-daemon-command-authority.js';
87
88
  import { resolveDaemonIpcPort } from './utils/daemon-discovery.js';
88
89
  import { inspectBotmuxPm2Apps, isExactPm2BotActivationReceipt, managedActivationPm2Disposition, parsePm2JlistOutputStrict as parsePm2JlistOutput, stopExactPm2Process, } from './core/bot-live-control.js';
90
+ import { isSuspendableBackendType, killPersistentBackendTarget, probePersistentBackendTarget, probePersistentSessions, resolvePersistentBackendTarget, } from './core/persistent-backend.js';
89
91
  // Resolve the CLI's UI locale once from the global config file, so subsequent
90
92
  // CLI output (and any t() callers that don't pass an explicit locale) honour
91
93
  // the user's chosen language. Daemon entrypoint sets this separately for the
@@ -329,6 +331,85 @@ function ensureUniqueBotProcessNames(bots) {
329
331
  }
330
332
  }
331
333
  }
334
+ /**
335
+ * `botmux serve --api-only` — run a single-process, headless core-only service
336
+ * in the FOREGROUND (stdio inherited so a launcher can watch the ready line and
337
+ * the process lifetime IS the service). No pm2, no dashboard, no bots.json, no
338
+ * Feishu credentials. See src/index-core-only.ts for the full contract.
339
+ */
340
+ async function cmdServe(args) {
341
+ const apiOnly = args.includes('--api-only');
342
+ if (!apiOnly) {
343
+ console.error('Usage: botmux serve --api-only [--port <PORT>] [--bot <local_slug>] [--cli <cliId>] [--state-dir <DIR>]');
344
+ console.error(' Only core-only (--api-only) serving is supported. It runs a headless HTTP');
345
+ console.error(' control-API service with no Feishu credentials (for riff sandbox / embedding).');
346
+ process.exit(2);
347
+ }
348
+ const getOpt = (flag) => {
349
+ const i = args.indexOf(flag);
350
+ return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
351
+ };
352
+ const port = getOpt('--port') ?? process.env.BOTMUX_API_PORT;
353
+ if (!port || !/^\d+$/.test(port) || Number(port) < 1 || Number(port) > 65535) {
354
+ console.error(`botmux serve --api-only: --port (or BOTMUX_API_PORT) must be a valid port; got: ${port ?? '(unset)'}`);
355
+ process.exit(2);
356
+ }
357
+ const bot = getOpt('--bot') ?? process.env.BOTMUX_API_ONLY_BOT;
358
+ const cli = getOpt('--cli') ?? process.env.BOTMUX_CORE_CLI;
359
+ const workingDir = getOpt('--working-dir') ?? process.env.BOTMUX_CORE_WORKING_DIR;
360
+ const stateDir = getOpt('--state-dir') ?? process.env.BOTMUX_CORE_STATE_DIR;
361
+ const coreScript = join(PKG_ROOT, 'dist', 'index-core-only.js');
362
+ const child = spawn(process.execPath, [coreScript], {
363
+ stdio: 'inherit',
364
+ env: (() => {
365
+ const e = {
366
+ ...process.env,
367
+ BOTMUX_CORE_ONLY: '1',
368
+ BOTMUX_API_PORT: port,
369
+ // Freeze worker HTTP to loopback here too (defense-in-depth with the
370
+ // entrypoint) so a stray parent/dotenv 0.0.0.0 never reaches the child.
371
+ BOTMUX_WORKER_HTTP_HOST: '127.0.0.1',
372
+ ...(bot ? { BOTMUX_API_ONLY_BOT: bot } : {}),
373
+ ...(cli ? { BOTMUX_CORE_CLI: cli } : {}),
374
+ ...(workingDir ? { BOTMUX_CORE_WORKING_DIR: workingDir } : {}),
375
+ ...(stateDir ? { BOTMUX_CORE_STATE_DIR: stateDir } : {}),
376
+ };
377
+ // Never hand an ambient BOTS_CONFIG / legacy worker-host alias / ambient
378
+ // SESSION_DATA_DIR to the core-only child — the entrypoint freezes/strips
379
+ // these too, but keep the spawn env clean from the start (codex P1: agent
380
+ // could read $BOTS_CONFIG; ambient SESSION_DATA_DIR would point at a host
381
+ // fleet's store). The entrypoint re-derives a dedicated core-only state root.
382
+ delete e.BOTS_CONFIG;
383
+ delete e.BOTMUX_WORKER_HOST;
384
+ delete e.SESSION_DATA_DIR;
385
+ return e;
386
+ })(),
387
+ });
388
+ // Foreground lifetime tracks the child: forward termination signals and exit
389
+ // with the child's code so a launcher/supervisor sees an honest status.
390
+ const forward = (sig) => { try {
391
+ child.kill(sig);
392
+ }
393
+ catch { /* */ } };
394
+ process.on('SIGTERM', () => forward('SIGTERM'));
395
+ process.on('SIGINT', () => forward('SIGINT'));
396
+ await new Promise((resolve) => {
397
+ child.on('exit', (code, signal) => {
398
+ if (signal) {
399
+ process.exitCode = 1;
400
+ }
401
+ else {
402
+ process.exitCode = code ?? 0;
403
+ }
404
+ resolve();
405
+ });
406
+ child.on('error', (err) => {
407
+ console.error(`[core-only] failed to spawn service: ${err.message}`);
408
+ process.exitCode = 1;
409
+ resolve();
410
+ });
411
+ });
412
+ }
332
413
  function ecosystemConfig(activationAppId) {
333
414
  const daemonScript = join(PKG_ROOT, 'dist', 'index-daemon.js');
334
415
  const bots = loadBotsJson();
@@ -1188,9 +1269,9 @@ async function promptEditBotConfig(rl, bot) {
1188
1269
  input.model = null;
1189
1270
  }
1190
1271
  printInputHelp('会话后端 backendType', [
1191
- '可选。pty 更轻量;tmux 支持 adopt 和 Web Terminal 附着;herdr 支持托管持久会话;zellij 为实验后端(需 zellij >= 0.44)。',
1272
+ '可选。pty 更轻量;tmux 支持 adopt 和 Web Terminal 附着;herdr 支持托管持久会话;zmx >= 0.7.0 提供纯文本持久会话 + 本机 attach(无 Web TUI);zellij 为实验后端(需 zellij >= 0.44)。',
1192
1273
  '选择 traex + herdr 时,可在 Dashboard Settings 中开启 TraeX herdr plugin opt-in 并填写可信插件 spec;默认不会自动安装第三方插件。',
1193
- '留空保留当前值;输入 - 回到自动检测;接受 pty / tmux / herdr / zellij。',
1274
+ '留空保留当前值;输入 - 回到全局默认(未设置 BACKEND_TYPE 时为 tmux);接受 pty / tmux / herdr / zellij / zmx。',
1194
1275
  ]);
1195
1276
  input.backendType = await ask(rl, `会话后端 backendType [${formatOptionalValue(bot.backendType)}]: `);
1196
1277
  // 新话题工作目录:模式二选一(与 promptBotConfig 的新建流程同款问法)。
@@ -3103,7 +3184,7 @@ function loadBotConfigsForDisplay() {
3103
3184
  return [];
3104
3185
  }
3105
3186
  /** Format a single session row for display (used by both plain table and TUI). */
3106
- function formatSessionRow(s, multiBot, botLabels, cols) {
3187
+ function formatSessionRow(s, multiBot, botLabels, cols, probeSnapshot) {
3107
3188
  const id = padEndDisplay(s.sessionId.substring(0, 8), cols.id);
3108
3189
  const parts = [id];
3109
3190
  if (multiBot) {
@@ -3117,12 +3198,12 @@ function formatSessionRow(s, multiBot, botLabels, cols) {
3117
3198
  const uptime = formatDuration(Date.now() - new Date(s.createdAt).getTime()).padEnd(cols.uptime);
3118
3199
  const alive = isSessionAliveForList(s);
3119
3200
  const status = padEndDisplay(sessionStatusLabel(s), cols.status);
3120
- const target = padEndDisplay(truncate(sessionTargetLabel(s), cols.target), cols.target);
3201
+ const target = padEndDisplay(truncate(sessionTargetLabel(s, probeSnapshot), cols.target), cols.target);
3121
3202
  parts.push(title, dir, pid, uptime, status, target);
3122
3203
  return { text: parts.join(' │ '), alive };
3123
3204
  }
3124
3205
  /** Print plain session table (non-interactive). */
3125
- function printSessionTable(active) {
3206
+ function printSessionTable(active, probeSnapshot) {
3126
3207
  const botConfigs = loadBotConfigsForDisplay();
3127
3208
  const multiBot = botConfigs.length > 1 || new Set(active.map(s => s.larkAppId).filter(Boolean)).size > 1;
3128
3209
  const botLabels = new Map();
@@ -3141,22 +3222,12 @@ function printSessionTable(active) {
3141
3222
  console.log(header);
3142
3223
  console.log(separator);
3143
3224
  for (const s of active) {
3144
- const { text } = formatSessionRow(s, multiBot, botLabels, cols);
3225
+ const { text } = formatSessionRow(s, multiBot, botLabels, cols, probeSnapshot);
3145
3226
  console.log(text);
3146
3227
  }
3147
3228
  console.log(separator);
3148
3229
  console.log(`共 ${active.length} 个活跃会话`);
3149
3230
  }
3150
- /** Check if a tmux session exists. */
3151
- function tmuxSessionExists(name) {
3152
- try {
3153
- execSync(`tmux has-session -t ${name} 2>/dev/null`, { stdio: 'ignore', env: tmuxEnv() });
3154
- return true;
3155
- }
3156
- catch {
3157
- return false;
3158
- }
3159
- }
3160
3231
  function applyTmuxWindowSizeLargest(sessionName) {
3161
3232
  try {
3162
3233
  execFileSync('tmux', ['set-option', '-t', sessionName, 'window-size', 'largest'], {
@@ -3188,18 +3259,41 @@ function closeSessionOffline(s) {
3188
3259
  // Adopted panes belong to the user. Ordinary bmx-* sessions are botmux-owned
3189
3260
  // and still need direct cleanup when no daemon exists to run killWorker().
3190
3261
  if (!isAdoptedSession(s)) {
3191
- const tmuxName = `bmx-${s.sessionId.substring(0, 8)}`;
3192
- try {
3193
- execSync(`tmux kill-session -t '${tmuxName}' 2>/dev/null`, {
3194
- stdio: 'ignore',
3195
- env: tmuxEnv(),
3196
- });
3262
+ if (isSuspendableBackendType(s.backendType)) {
3263
+ const target = resolvePersistentBackendTarget(s.backendType, s.sessionId, s.persistentBackendTarget);
3264
+ try {
3265
+ killPersistentBackendTarget(target, s.sessionId);
3266
+ }
3267
+ catch (err) {
3268
+ // ZMX destruction is identity-verified against the complete botmux
3269
+ // session UUID and waits for confirmed disappearance. Swallowing an
3270
+ // inconclusive/mismatched kill would hide the sole control row while
3271
+ // its CLI remains alive. Older mux backends retain their historical
3272
+ // best-effort offline-close compatibility.
3273
+ if (target.backendType === 'zmx') {
3274
+ return {
3275
+ ok: false,
3276
+ error: `ZMX 离线删除未完成:${err instanceof Error ? err.message : String(err)}`,
3277
+ };
3278
+ }
3279
+ }
3280
+ }
3281
+ else {
3282
+ // Legacy rows without backendType were historically tmux-backed.
3283
+ const tmuxName = `bmx-${s.sessionId.substring(0, 8)}`;
3284
+ try {
3285
+ execSync(`tmux kill-session -t '${tmuxName}' 2>/dev/null`, {
3286
+ stdio: 'ignore',
3287
+ env: tmuxEnv(),
3288
+ });
3289
+ }
3290
+ catch { /* no tmux session */ }
3197
3291
  }
3198
- catch { /* no tmux session */ }
3199
3292
  }
3200
3293
  s.status = 'closed';
3201
3294
  s.closedAt = new Date().toISOString();
3202
3295
  saveSession(s);
3296
+ return { ok: true, via: 'offline' };
3203
3297
  }
3204
3298
  /** Close through the owning daemon whenever it is online. The IPC request is
3205
3299
  * deliberately sent BEFORE any local kill: deleting the current bmx-* tmux
@@ -3221,8 +3315,7 @@ async function closeSessionForDelete(s, online = listOnlineDaemons()) {
3221
3315
  // port); it only keeps larkAppId-less legacy records on the offline fallback,
3222
3316
  // whose saveSession() persists to the legacy file correctly.
3223
3317
  if (!s.larkAppId) {
3224
- closeSessionOffline(s);
3225
- return { ok: true, via: 'offline' };
3318
+ return closeSessionOffline(s);
3226
3319
  }
3227
3320
  const daemon = online.find(d => d.larkAppId === s.larkAppId);
3228
3321
  const isCurrentSession = process.env.BOTMUX_SESSION_ID === s.sessionId;
@@ -3244,8 +3337,7 @@ async function closeSessionForDelete(s, online = listOnlineDaemons()) {
3244
3337
  return { ok: false, error: `连接 daemon 失败: ${err?.message ?? err}` };
3245
3338
  }
3246
3339
  }
3247
- closeSessionOffline(s);
3248
- return { ok: true, via: 'offline' };
3340
+ return closeSessionOffline(s);
3249
3341
  }
3250
3342
  function adoptTargetLabel(s) {
3251
3343
  if (!isAdoptedSession(s))
@@ -3282,15 +3374,117 @@ function sessionStatusLabel(s) {
3282
3374
  return 'dormant';
3283
3375
  return s.pid && isProcessAlive(s.pid) ? 'online' : s.pid ? 'stopped' : 'idle';
3284
3376
  }
3285
- function sessionTargetLabel(s, tmuxName, hasTmux) {
3377
+ function backingProbeKey(target) {
3378
+ const agentName = target.backendType === 'herdr' ? target.agentName ?? '' : '';
3379
+ return `${target.backendType}\0${target.sessionName}\0${agentName}`;
3380
+ }
3381
+ function sessionPersistentTarget(s) {
3382
+ if (isSuspendableBackendType(s.backendType)) {
3383
+ return resolvePersistentBackendTarget(s.backendType, s.sessionId, s.persistentBackendTarget);
3384
+ }
3385
+ if (s.backendType === undefined) {
3386
+ // Legacy rows predate backend stamping. Only tmux was externally
3387
+ // attachable, and its deterministic target remains the compatibility path.
3388
+ return {
3389
+ backendType: 'tmux',
3390
+ sessionName: `bmx-${s.sessionId.substring(0, 8)}`,
3391
+ };
3392
+ }
3393
+ return undefined;
3394
+ }
3395
+ function persistentTargetDisplay(target) {
3396
+ return target.backendType === 'herdr' && target.agentName
3397
+ ? `${target.sessionName}/${target.agentName}`
3398
+ : target.sessionName;
3399
+ }
3400
+ function buildBackingProbeSnapshot(sessions) {
3401
+ const namesByBackend = new Map();
3402
+ const directTargets = new Map();
3403
+ const add = (target) => {
3404
+ if (target.backendType === 'herdr' && target.agentName) {
3405
+ directTargets.set(backingProbeKey(target), target);
3406
+ return;
3407
+ }
3408
+ const backendType = target.backendType;
3409
+ const name = target.sessionName;
3410
+ const names = namesByBackend.get(backendType) ?? new Set();
3411
+ names.add(name);
3412
+ namesByBackend.set(backendType, names);
3413
+ };
3414
+ for (const session of sessions) {
3415
+ if (isAdoptedSession(session) || session.backendType === 'pty')
3416
+ continue;
3417
+ const target = sessionPersistentTarget(session);
3418
+ if (target)
3419
+ add(target);
3420
+ }
3421
+ const snapshot = new Map();
3422
+ // Agent-scoped Herdr targets cannot be collapsed into a host-session probe:
3423
+ // the shared host may be healthy after this exact Botmux agent exited.
3424
+ for (const [key, target] of directTargets) {
3425
+ snapshot.set(key, probePersistentBackendTarget(target));
3426
+ }
3427
+ for (const [backendType, names] of namesByBackend) {
3428
+ for (const [name, probe] of probePersistentSessions(backendType, names)) {
3429
+ snapshot.set(backingProbeKey({ backendType, sessionName: name }), probe);
3430
+ }
3431
+ }
3432
+ return snapshot;
3433
+ }
3434
+ function backingProbe(snapshot, target) {
3435
+ return snapshot?.get(backingProbeKey(target))
3436
+ ?? probePersistentBackendTarget(target);
3437
+ }
3438
+ function sessionBackingInfo(s, snapshot) {
3439
+ if (isSuspendableBackendType(s.backendType)) {
3440
+ const target = sessionPersistentTarget(s);
3441
+ const probe = backingProbe(snapshot, target);
3442
+ const suffix = probe === 'exists' ? '' : ` (${probe})`;
3443
+ return {
3444
+ backendType: s.backendType,
3445
+ target,
3446
+ probe,
3447
+ label: `${s.backendType}: ${persistentTargetDisplay(target)}${suffix}`,
3448
+ attachBackend: s.backendType === 'tmux' || s.backendType === 'zmx'
3449
+ ? s.backendType
3450
+ : undefined,
3451
+ };
3452
+ }
3453
+ if (s.backendType === 'pty') {
3454
+ return { backendType: 'pty', probe: 'missing', label: 'pty' };
3455
+ }
3456
+ // Legacy rows predate backend stamping. Only tmux was externally attachable.
3457
+ const target = sessionPersistentTarget(s);
3458
+ const probe = backingProbe(snapshot, target);
3459
+ return {
3460
+ backendType: 'tmux',
3461
+ target,
3462
+ probe,
3463
+ label: probe === 'exists' ? `tmux: ${target.sessionName}` : '-',
3464
+ attachBackend: 'tmux',
3465
+ };
3466
+ }
3467
+ function sessionTargetLabel(s, snapshot) {
3286
3468
  if (isAdoptedSession(s))
3287
3469
  return adoptTargetLabel(s);
3288
- if (hasTmux === undefined) {
3289
- const name = tmuxName ?? `bmx-${s.sessionId.substring(0, 8)}`;
3290
- hasTmux = tmuxSessionExists(name);
3291
- tmuxName = name;
3470
+ return sessionBackingInfo(s, snapshot).label;
3471
+ }
3472
+ function hasRecoverableBackingSession(s, snapshot) {
3473
+ if (isSuspendableBackendType(s.backendType)) {
3474
+ // Unknown means the backend probe itself was inconclusive; keep the session
3475
+ // rather than closing a potentially recoverable conversation from `list`.
3476
+ // ZMX has one daemon per session. A clean "missing" result cannot
3477
+ // distinguish a host reboot from an individual CLI exit, so keep the
3478
+ // transcript-backed row for lazy resume instead of auto-pruning it.
3479
+ if (s.backendType === 'zmx')
3480
+ return true;
3481
+ const target = sessionPersistentTarget(s);
3482
+ const probe = backingProbe(snapshot, target);
3483
+ return probe === 'exists' || probe === 'unknown';
3292
3484
  }
3293
- return hasTmux ? `tmux: ${tmuxName}` : '-';
3485
+ // Legacy sessions created before backendType stamping only had tmux recovery.
3486
+ const target = sessionPersistentTarget(s);
3487
+ return !!target && backingProbe(snapshot, target) === 'exists';
3294
3488
  }
3295
3489
  /** Shorten path for display: replace $HOME with ~. */
3296
3490
  function shortenPath(p) {
@@ -3298,7 +3492,7 @@ function shortenPath(p) {
3298
3492
  return p.startsWith(home) ? '~' + p.slice(home.length) : p;
3299
3493
  }
3300
3494
  /** Interactive TUI session picker — returns a promise that resolves when done. */
3301
- function interactiveSessionPicker(active) {
3495
+ function interactiveSessionPicker(active, probeSnapshot) {
3302
3496
  const botConfigs = loadBotConfigsForDisplay();
3303
3497
  const multiBot = botConfigs.length > 1 || new Set(active.map(s => s.larkAppId).filter(Boolean)).size > 1;
3304
3498
  const botLabels = new Map();
@@ -3330,10 +3524,11 @@ function interactiveSessionPicker(active) {
3330
3524
  // Build row data — use shortened paths for TUI
3331
3525
  function buildRows() {
3332
3526
  return active.map(s => {
3333
- const tmuxName = `bmx-${s.sessionId.substring(0, 8)}`;
3334
3527
  const isAdopt = isAdoptedSession(s);
3335
- const hasTmux = !isAdopt && tmuxSessionExists(tmuxName);
3336
- const targetLabel = sessionTargetLabel(s, tmuxName, hasTmux);
3528
+ const backing = isAdopt
3529
+ ? { probe: 'missing', label: adoptTargetLabel(s) }
3530
+ : sessionBackingInfo(s, probeSnapshot);
3531
+ const targetLabel = backing.label;
3337
3532
  // Build row text with shortened dir
3338
3533
  const id = padEndDisplay(s.sessionId.substring(0, 8), cols.id);
3339
3534
  const parts = [id];
@@ -3350,7 +3545,20 @@ function interactiveSessionPicker(active) {
3350
3545
  const status = padEndDisplay(sessionStatusLabel(s), cols.status);
3351
3546
  const target = padEndDisplay(truncate(targetLabel, cols.target), cols.target);
3352
3547
  parts.push(title, dir, pid, uptime, status, target);
3353
- return { session: s, text: parts.join(' │ '), alive, tmuxName, hasTmux, isAdopt, targetLabel, canAttach: hasTmux && !isAdopt };
3548
+ return {
3549
+ session: s,
3550
+ text: parts.join(' │ '),
3551
+ alive,
3552
+ backendTarget: 'target' in backing ? backing.target : undefined,
3553
+ backingProbe: backing.probe,
3554
+ attachBackend: 'attachBackend' in backing ? backing.attachBackend : undefined,
3555
+ isAdopt,
3556
+ targetLabel,
3557
+ canAttach: !isAdopt
3558
+ && backing.probe === 'exists'
3559
+ && !!('attachBackend' in backing && backing.attachBackend)
3560
+ && !!('target' in backing && backing.target),
3561
+ };
3354
3562
  });
3355
3563
  }
3356
3564
  let rows = buildRows();
@@ -3395,9 +3603,9 @@ function interactiveSessionPicker(active) {
3395
3603
  const selected = rows[cursor];
3396
3604
  const targetHint = selected.isAdopt
3397
3605
  ? `\x1b[33m${selected.targetLabel}\x1b[0m \x1b[2mEnter 已禁用;请直接使用原 tmux/zellij/herdr 客户端。\x1b[0m`
3398
- : selected.hasTmux
3399
- ? `\x1b[32mtmux: ${selected.tmuxName}\x1b[0m`
3400
- : `\x1b[2mtmux: 无会话\x1b[0m`;
3606
+ : selected.canAttach
3607
+ ? `\x1b[32m${selected.attachBackend}: ${selected.backendTarget?.sessionName}\x1b[0m`
3608
+ : `\x1b[2m${selected.targetLabel}(不可连接)\x1b[0m`;
3401
3609
  process.stdout.write(`\n ${targetHint}\n`);
3402
3610
  // Flash message or confirmation prompt
3403
3611
  if (confirmDelete) {
@@ -3497,7 +3705,7 @@ function interactiveSessionPicker(active) {
3497
3705
  render();
3498
3706
  return;
3499
3707
  }
3500
- // Enter — attach to tmux
3708
+ // Enter — attach to a managed persistent backend.
3501
3709
  if (key === '\r' || key === '\n') {
3502
3710
  const selected = rows[cursor];
3503
3711
  if (selected.isAdopt) {
@@ -3506,26 +3714,85 @@ function interactiveSessionPicker(active) {
3506
3714
  return;
3507
3715
  }
3508
3716
  if (!selected.canAttach) {
3509
- flashMsg = '\x1b[33m该会话没有 tmux,无法连接\x1b[0m';
3717
+ flashMsg = '\x1b[33m该会话没有可连接的持久后端\x1b[0m';
3510
3718
  render();
3511
3719
  return;
3512
3720
  }
3513
- applyTmuxWindowSizeLargest(selected.tmuxName);
3514
- cleanup();
3515
- spawnSync('tmux', ['attach-session', '-t', selected.tmuxName], {
3516
- stdio: 'inherit',
3517
- env: tmuxEnv(),
3518
- });
3721
+ if (selected.attachBackend === 'zmx') {
3722
+ const target = selected.backendTarget;
3723
+ if (!target || target.backendType !== 'zmx') {
3724
+ flashMsg = '\x1b[31mZMX attach target is missing or inconsistent\x1b[0m';
3725
+ render();
3726
+ return;
3727
+ }
3728
+ // First prove both complete Botmux labels while the picker is still
3729
+ // active, then freeze the PTY root generation across terminal
3730
+ // cleanup and re-prove it immediately before attach.
3731
+ const frozen = freezeManagedZmxAttachTarget(target.sessionName, selected.session.sessionId);
3732
+ if (!frozen.ok) {
3733
+ flashMsg = `\x1b[31m${frozen.message}\x1b[0m`;
3734
+ render();
3735
+ return;
3736
+ }
3737
+ cleanup();
3738
+ const attached = attachFrozenManagedZmxSession(target.sessionName, selected.session.sessionId, frozen.pid);
3739
+ if (!attached.ok)
3740
+ console.error(attached.message);
3741
+ }
3742
+ else {
3743
+ const target = selected.backendTarget;
3744
+ if (!target || target.backendType !== 'tmux') {
3745
+ flashMsg = '\x1b[31mtmux attach target is missing or inconsistent\x1b[0m';
3746
+ render();
3747
+ return;
3748
+ }
3749
+ cleanup();
3750
+ applyTmuxWindowSizeLargest(target.sessionName);
3751
+ spawnSync('tmux', ['attach-session', '-t', `=${target.sessionName}`], {
3752
+ stdio: 'inherit',
3753
+ env: tmuxEnv(),
3754
+ });
3755
+ }
3519
3756
  resolve();
3520
3757
  return;
3521
3758
  }
3522
3759
  });
3523
3760
  });
3524
3761
  }
3762
+ /**
3763
+ * Internal host-only bridge used by Dashboard "Open CLI" commands. The
3764
+ * generated terminal shell executes this exact checkout's cli.js, keeping all
3765
+ * ZMX ownership checks in TypeScript instead of approximating them with
3766
+ * name-only shell pipelines.
3767
+ */
3768
+ function cmdManagedZmxAttach(args) {
3769
+ const [name, sessionId, ...extra] = args;
3770
+ if (!name?.trim() || !sessionId?.trim() || extra.length > 0) {
3771
+ console.error('internal usage: __zmx-attach-managed <session-name> <complete-session-id>');
3772
+ process.exitCode = 2;
3773
+ return;
3774
+ }
3775
+ const frozen = freezeManagedZmxAttachTarget(name, sessionId);
3776
+ if (!frozen.ok) {
3777
+ console.error(frozen.message);
3778
+ process.exitCode = 1;
3779
+ return;
3780
+ }
3781
+ const attached = attachFrozenManagedZmxSession(name, sessionId, frozen.pid);
3782
+ if (!attached.ok) {
3783
+ console.error(attached.message);
3784
+ process.exitCode = 1;
3785
+ }
3786
+ }
3525
3787
  async function cmdList() {
3526
3788
  const sessions = loadSessions();
3527
3789
  const active = [...sessions.values()].filter(s => s.status === 'active');
3528
- // Auto-prune unrecoverable sessions: process dead and no tmux session.
3790
+ // One immutable control-plane snapshot per invocation. In particular, ZMX's
3791
+ // full-list probe walks every per-session daemon, so running it once per row
3792
+ // would make a large session list quadratic and amplify socket timeouts.
3793
+ const probeSnapshot = buildBackingProbeSnapshot(active);
3794
+ // Auto-prune unrecoverable sessions: process dead and no recoverable backing
3795
+ // session (tmux/herdr/zellij/zmx).
3529
3796
  // Split into two buckets so a never-activated daemon-command scratch (e.g. an
3530
3797
  // unconfirmed /adopt that only posted a picker card, /help, an abandoned
3531
3798
  // /relay picker) isn't reported as a crashed CLI. Such a scratch never forked
@@ -3552,8 +3819,8 @@ async function cmdList() {
3552
3819
  continue;
3553
3820
  }
3554
3821
  const hasPid = !!(s.pid && isProcessAlive(s.pid));
3555
- const hasTmux = tmuxSessionExists(`bmx-${s.sessionId.substring(0, 8)}`);
3556
- const disposition = sessionListDisposition(s, { hasPid, hasBackingSession: hasTmux });
3822
+ const hasBackingSession = hasRecoverableBackingSession(s, probeSnapshot);
3823
+ const disposition = sessionListDisposition(s, { hasPid, hasBackingSession });
3557
3824
  if (disposition === 'prune_real')
3558
3825
  pruned.push(s);
3559
3826
  else if (disposition === 'prune_scratch')
@@ -3561,18 +3828,30 @@ async function cmdList() {
3561
3828
  else
3562
3829
  live.push(s);
3563
3830
  }
3564
- const closeNow = (arr) => {
3831
+ const closeNow = async (arr, kind) => {
3832
+ let closed = 0;
3565
3833
  for (const s of arr) {
3566
- s.status = 'closed';
3567
- s.closedAt = new Date().toISOString();
3568
- saveSession(s);
3834
+ const result = await closeSessionForDelete(s);
3835
+ if (result.ok) {
3836
+ closed++;
3837
+ }
3838
+ else {
3839
+ // Keep it visible: mutating only the store while a possible owner
3840
+ // daemon still has the row in memory lets the next message resurrect
3841
+ // exactly the session auto-prune claimed to close.
3842
+ live.push(s);
3843
+ console.warn(`⚠️ 未自动清理 ${kind === 'scratch' ? '占位' : '会话'} ${s.sessionId.substring(0, 8)}:${result.error}`);
3844
+ }
3569
3845
  }
3846
+ return closed;
3570
3847
  };
3571
3848
  // Scratches: close silently — they were placeholders, not dead sessions.
3572
- closeNow(prunedScratch);
3849
+ await closeNow(prunedScratch, 'scratch');
3573
3850
  if (pruned.length > 0) {
3574
- closeNow(pruned);
3575
- console.log(`已自动清理 ${pruned.length} 个不可恢复的会话(进程已退出或无可恢复后端)`);
3851
+ const closed = await closeNow(pruned, 'real');
3852
+ if (closed > 0) {
3853
+ console.log(`已自动清理 ${closed} 个不可恢复的会话(进程已退出或无可恢复后端)`);
3854
+ }
3576
3855
  }
3577
3856
  // Sort by creation time, newest first
3578
3857
  live.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
@@ -3582,11 +3861,11 @@ async function cmdList() {
3582
3861
  }
3583
3862
  // Non-TTY (piped output) or explicit --plain flag: plain table
3584
3863
  if (!process.stdout.isTTY || process.argv.includes('--plain')) {
3585
- printSessionTable(live);
3864
+ printSessionTable(live, probeSnapshot);
3586
3865
  return;
3587
3866
  }
3588
3867
  // Interactive TUI
3589
- await interactiveSessionPicker(live);
3868
+ await interactiveSessionPicker(live, probeSnapshot);
3590
3869
  }
3591
3870
  async function cmdDelete() {
3592
3871
  const target = process.argv[3];
@@ -3596,6 +3875,7 @@ async function cmdDelete() {
3596
3875
  }
3597
3876
  const sessions = loadSessions();
3598
3877
  const active = [...sessions.values()].filter(s => s.status === 'active');
3878
+ const probeSnapshot = buildBackingProbeSnapshot(active);
3599
3879
  if (active.length === 0) {
3600
3880
  console.log('没有活跃会话。');
3601
3881
  return;
@@ -3618,8 +3898,7 @@ async function cmdDelete() {
3618
3898
  return pid ? !isProcessAlive(pid) : !(s.pid && isProcessAlive(s.pid));
3619
3899
  }
3620
3900
  const hasPid = !!(s.pid && isProcessAlive(s.pid));
3621
- const hasTmux = tmuxSessionExists(`bmx-${s.sessionId.substring(0, 8)}`);
3622
- return !hasPid && !hasTmux;
3901
+ return !hasPid && !hasRecoverableBackingSession(s, probeSnapshot);
3623
3902
  });
3624
3903
  if (toDelete.length === 0) {
3625
3904
  console.log('没有 stopped 状态的会话。');
@@ -3809,6 +4088,50 @@ async function postSessionCliIpc(ipcPort, sessionId, route, payload) {
3809
4088
  ? fetchDaemonIpc(ipcPort, path, init, hostSecret)
3810
4089
  : fetch(`http://127.0.0.1:${ipcPort}${path}`, init);
3811
4090
  }
4091
+ async function cmdChat(argv) {
4092
+ const sub = argv[0] ?? '';
4093
+ if (sub !== 'rename') {
4094
+ console.error('用法: botmux chat rename <新群名称> [--proactive]');
4095
+ process.exitCode = 2;
4096
+ return;
4097
+ }
4098
+ const proactive = argv.includes('--proactive');
4099
+ const name = argv.slice(1).filter(arg => arg !== '--proactive').join(' ').trim();
4100
+ if (!name) {
4101
+ console.error('用法: botmux chat rename <新群名称> [--proactive]');
4102
+ process.exitCode = 2;
4103
+ return;
4104
+ }
4105
+ const ctx = findAncestorSessionContext();
4106
+ const sid = ctx?.sessionId;
4107
+ if (!sid) {
4108
+ console.error(JSON.stringify({ ok: false, error: 'missing_session_context' }));
4109
+ process.exitCode = 1;
4110
+ return;
4111
+ }
4112
+ const sessions = loadSessions();
4113
+ const session = [...sessions.values()].find(x => x.sessionId === sid || x.sessionId.startsWith(sid));
4114
+ if (!session) {
4115
+ console.error(JSON.stringify({ ok: false, error: 'missing_session_context' }));
4116
+ process.exitCode = 1;
4117
+ return;
4118
+ }
4119
+ const daemon = findDaemon(session.larkAppId);
4120
+ if (!daemon) {
4121
+ console.error(JSON.stringify({ ok: false, error: 'daemon_offline' }));
4122
+ process.exitCode = 1;
4123
+ return;
4124
+ }
4125
+ const response = await postSessionCliIpc(daemon.ipcPort, session.sessionId, 'chat-rename', { name, proactive });
4126
+ const body = await response.json().catch(() => ({ ok: false, error: `HTTP ${response.status}` }));
4127
+ const out = JSON.stringify(body, null, 2);
4128
+ if (response.ok && body?.ok) {
4129
+ console.log(out);
4130
+ return;
4131
+ }
4132
+ console.error(out);
4133
+ process.exitCode = 1;
4134
+ }
3812
4135
  /** botmux slash "<斜杠命令>":请求 daemon 在本会话 idle 后把命令敲入自己的 CLI。
3813
4136
  * 自识别当前会话(pid marker → BOTMUX_SESSION_ID env),allowlist 由 daemon 侧校验。 */
3814
4137
  async function cmdSlash() {
@@ -3892,19 +4215,10 @@ async function cmdRoleSwitch(argv) {
3892
4215
  console.error(`✗ 切换被拒绝: ${body?.error ?? `HTTP ${res.status}`}`);
3893
4216
  process.exit(1);
3894
4217
  }
3895
- /**
3896
- * Discover online daemons. Mirrors the staleness rule used by
3897
- * dashboard/registry.ts (90s heartbeat) so we don't try to talk to a daemon
3898
- * that's been dead but left a stale descriptor behind. Uses resolveDataDir()
3899
- * so SESSION_DATA_DIR / breadcrumb-overridden deployments find the right
3900
- * descriptor directory.
3901
- */
3902
- function listOnlineDaemons() {
4218
+ function listDaemonDescriptors() {
3903
4219
  const regDir = join(resolveDataDir(), 'dashboard-daemons');
3904
4220
  if (!existsSync(regDir))
3905
4221
  return [];
3906
- const STALE_MS = 90_000;
3907
- const now = Date.now();
3908
4222
  const all = [];
3909
4223
  let names = [];
3910
4224
  try {
@@ -3920,30 +4234,131 @@ function listOnlineDaemons() {
3920
4234
  const d = JSON.parse(readFileSync(join(regDir, f), 'utf-8'));
3921
4235
  if (typeof d?.ipcPort !== 'number' || typeof d?.larkAppId !== 'string')
3922
4236
  continue;
3923
- if (now - (d.lastHeartbeat ?? 0) > STALE_MS)
3924
- continue;
3925
4237
  all.push({
3926
4238
  ipcPort: d.ipcPort,
3927
4239
  larkAppId: d.larkAppId,
4240
+ ...(typeof d.pid === 'number' ? { pid: d.pid } : {}),
3928
4241
  ...(typeof d.bootInstanceId === 'string' && d.bootInstanceId
3929
4242
  ? { bootInstanceId: d.bootInstanceId }
3930
4243
  : {}),
3931
4244
  ...(typeof d.workflowIpcProtocol === 'string' && d.workflowIpcProtocol
3932
4245
  ? { workflowIpcProtocol: d.workflowIpcProtocol }
3933
4246
  : {}),
3934
- lastHeartbeat: d.lastHeartbeat,
4247
+ ...(typeof d.lastHeartbeat === 'number' ? { lastHeartbeat: d.lastHeartbeat } : {}),
3935
4248
  });
3936
4249
  }
3937
4250
  catch { /* skip malformed */ }
3938
4251
  }
3939
4252
  return all;
3940
4253
  }
4254
+ function listOnlineDaemons() {
4255
+ const STALE_MS = 90_000;
4256
+ const now = Date.now();
4257
+ return listDaemonDescriptors().filter(d => now - (d.lastHeartbeat ?? 0) <= STALE_MS);
4258
+ }
3941
4259
  function findDaemon(larkAppId) {
3942
4260
  const all = listOnlineDaemons();
3943
4261
  if (larkAppId)
3944
4262
  return all.find(d => d.larkAppId === larkAppId) ?? null;
3945
4263
  return all[0] ?? null;
3946
4264
  }
4265
+ function normalizeCardUsageSnapshot(value) {
4266
+ if (!value || typeof value !== 'object' || Array.isArray(value))
4267
+ return null;
4268
+ const raw = value;
4269
+ const rawContext = raw.context;
4270
+ const rawTokens = raw.tokens;
4271
+ let context = null;
4272
+ if (rawContext && typeof rawContext === 'object' && !Array.isArray(rawContext)) {
4273
+ const c = rawContext;
4274
+ if (typeof c.usedTokens === 'number'
4275
+ && Number.isFinite(c.usedTokens)
4276
+ && c.usedTokens >= 0) {
4277
+ context = {
4278
+ usedTokens: c.usedTokens,
4279
+ ...(typeof c.windowTokens === 'number'
4280
+ && Number.isFinite(c.windowTokens)
4281
+ && c.windowTokens > 0
4282
+ ? { windowTokens: c.windowTokens }
4283
+ : {}),
4284
+ ...(typeof c.percentUsed === 'number'
4285
+ && Number.isFinite(c.percentUsed)
4286
+ && c.percentUsed >= 0
4287
+ ? { percentUsed: c.percentUsed }
4288
+ : {}),
4289
+ };
4290
+ }
4291
+ }
4292
+ let tokens = null;
4293
+ if (rawTokens && typeof rawTokens === 'object' && !Array.isArray(rawTokens)) {
4294
+ const u = rawTokens;
4295
+ if (typeof u.in === 'number'
4296
+ && Number.isFinite(u.in)
4297
+ && u.in >= 0
4298
+ && typeof u.out === 'number'
4299
+ && Number.isFinite(u.out)
4300
+ && u.out >= 0) {
4301
+ tokens = { in: u.in, out: u.out };
4302
+ }
4303
+ }
4304
+ return { context, tokens };
4305
+ }
4306
+ /** Prefer the resident daemon's incremental transcript cache. Older/offline
4307
+ * daemons and isolated environments fall back to the local reader; either path
4308
+ * degrades to explicit unavailable facts without blocking the reply. */
4309
+ async function readCardUsageSnapshotForSend(session, larkAppId) {
4310
+ let daemonPort;
4311
+ try {
4312
+ daemonPort =
4313
+ findDaemon(larkAppId)?.ipcPort
4314
+ ?? resolveDaemonIpcPort(undefined, process.env.BOTMUX_DAEMON_IPC_PORT);
4315
+ }
4316
+ catch {
4317
+ // A stale/unreadable daemon registry must not prevent the reply.
4318
+ }
4319
+ if (daemonPort) {
4320
+ try {
4321
+ const path = `/api/sessions/${encodeURIComponent(session.sessionId)}/usage`;
4322
+ const response = await fetchDaemonIpc(daemonPort, path, {
4323
+ method: 'GET',
4324
+ signal: AbortSignal.timeout(1_500),
4325
+ });
4326
+ if (response.ok) {
4327
+ const body = await response.json();
4328
+ const normalized = normalizeCardUsageSnapshot(body.usage);
4329
+ if (normalized)
4330
+ return normalized;
4331
+ }
4332
+ }
4333
+ catch {
4334
+ // No host secret, old daemon, timeout, or transient IPC failure: use the
4335
+ // same bounded local parser below.
4336
+ }
4337
+ }
4338
+ // Old/offline daemon fallback. Sandboxed panes receive this per-bot value
4339
+ // explicitly from the worker because bots.json is intentionally unreadable.
4340
+ // This send path renders into the reply-card FOOTER, so only the 'footer'
4341
+ // display mode surfaces usage here; 'streaming' shows it on the daemon's live
4342
+ // card (absent on this offline fallback) and 'off' shows nothing.
4343
+ if (resolveUsageDisplay(larkAppId) !== 'footer') {
4344
+ return { context: null, tokens: null };
4345
+ }
4346
+ try {
4347
+ return getSessionUsageSnapshot({
4348
+ cliId: (session.cliId ?? session.adoptedFrom?.cliId ?? 'unknown'),
4349
+ sessionId: session.sessionId,
4350
+ cliSessionId: session.cliSessionId ?? session.adoptedFrom?.sessionId,
4351
+ cwd: session.workingDir ?? session.adoptedFrom?.cwd,
4352
+ // BOT_HOME transcript fallback for CLI-data-redirected / sandboxed bots
4353
+ // (parity with the daemon reader and the ledger/dashboard consumers).
4354
+ larkAppId: larkAppId ?? session.larkAppId,
4355
+ fresh: true,
4356
+ });
4357
+ }
4358
+ catch {
4359
+ return { context: null, tokens: null };
4360
+ }
4361
+ }
3947
4362
  /**
3948
4363
  * Authenticate the human who opened this exact turn against the target run,
3949
4364
  * then return the only daemon app that may receive the mutation. Inherited
@@ -4361,6 +4776,9 @@ async function cmdResume() {
4361
4776
  else if (errCode === 'deferred_unmaterialized') {
4362
4777
  console.error('❌ 该静默定时轮次未创建话题,隐藏会话只保留审计记录,不能 resume。');
4363
4778
  }
4779
+ else if (errCode === 'resume_cancelled') {
4780
+ console.error('❌ 恢复过程中会话被关闭,本次 resume 已取消。');
4781
+ }
4364
4782
  else {
4365
4783
  console.error(`❌ 恢复失败: ${errCode}`);
4366
4784
  }
@@ -4461,6 +4879,9 @@ async function cmdTermLink(rest) {
4461
4879
  else if (errCode === 'terminal_unavailable') {
4462
4880
  console.error('❌ 该会话终端尚未就绪(worker 未起或缺 token)。等会话起来再试。');
4463
4881
  }
4882
+ else if (errCode === 'terminal_unsupported') {
4883
+ console.error('❌ 该会话后端不提供 Web 终端;ZMX 会话请在本机运行 botmux list 或 zmx attach。');
4884
+ }
4464
4885
  else if (errCode === 'no_owner') {
4465
4886
  console.error('❌ 该 bot 未配置 owner(allowedUsers 为空 / 全开放模式),没有可私密投递的对象。');
4466
4887
  }
@@ -4546,6 +4967,8 @@ botmux v${getVersion()} — IM ↔ AI 编程 CLI 桥接
4546
4967
  schedule run <id> 标记立即执行
4547
4968
 
4548
4969
  飞书消息(在 CLI 会话内自动推断 session):
4970
+ chat rename <新群名称> 修改当前会话所在群的名称
4971
+ --proactive 标记为 AI 主动改名(应用 10 分钟防抖)
4549
4972
  send [content] 发消息到当前话题(支持 stdin / --content-file)
4550
4973
  --images <path> 内联图片(可重复)
4551
4974
  --files <path> 附件(可重复)
@@ -5299,6 +5722,8 @@ async function resolveSessionAppId(sessionIdArg) {
5299
5722
  return { sid, larkAppId: s.larkAppId, session: s };
5300
5723
  }
5301
5724
  async function cmdHistory(rest) {
5725
+ // No-transport turn has no Feishu chat history to read — central hard gate.
5726
+ assertTurnTransportOrExit('history');
5302
5727
  // Read isolation: register this bot from its cred file so the Lark client is
5303
5728
  // available without reading the denied bots.json (same as cmdSend).
5304
5729
  await registerSelfFromCredFile();
@@ -5311,6 +5736,9 @@ async function cmdHistory(rest) {
5311
5736
  const scopeArg = argValue(rest, '--scope') ?? 'session';
5312
5737
  const sessionIdArg = argValue(rest, '--session-id');
5313
5738
  const { sid, larkAppId: appId, session: s } = await resolveSessionAppId(sessionIdArg);
5739
+ // Target-aware gate: a --session-id pointing at a virtual/apiOnly session must
5740
+ // be refused even from a normal turn (env gate above can't see the argument).
5741
+ assertSessionTransportOrExit({ chatId: s.chatId, larkAppId: appId }, 'history');
5314
5742
  const validScopes = new Set(['session', 'thread', 'chat', 'ambient']);
5315
5743
  if (!validScopes.has(scopeArg)) {
5316
5744
  console.error(`无效 --scope: ${scopeArg}。可用: session | thread | chat | ambient`);
@@ -5438,6 +5866,8 @@ async function cmdHistory(rest) {
5438
5866
  }
5439
5867
  }
5440
5868
  async function cmdQuoted(rest) {
5869
+ // No-transport turn cannot fetch a quoted Feishu message — central hard gate.
5870
+ assertTurnTransportOrExit('quoted');
5441
5871
  const sessionIdArg = argValue(rest, '--session-id');
5442
5872
  // Positional message_id is required. The id comes verbatim from the
5443
5873
  // `[用户引用了消息 用 botmux quoted om_xxx 查看]` prompt prefix the daemon
@@ -5455,7 +5885,9 @@ async function cmdQuoted(rest) {
5455
5885
  // bots.json — same as cmdHistory / cmdSend. Missing this was why a sandboxed
5456
5886
  // isolated bot's `botmux quoted` failed "Bot not registered".
5457
5887
  await registerSelfFromCredFile();
5458
- const { larkAppId: appId } = await resolveSessionAppId(sessionIdArg);
5888
+ const { larkAppId: appId, session: quotedSession } = await resolveSessionAppId(sessionIdArg);
5889
+ // Target-aware gate (see cmdHistory).
5890
+ assertSessionTransportOrExit({ chatId: quotedSession.chatId, larkAppId: appId }, 'quoted');
5459
5891
  const { getMessageDetail } = await import('./im/lark/client.js');
5460
5892
  const { expandMergeForward } = await import('./im/lark/merge-forward.js');
5461
5893
  const { renderQuotedMessage } = await import('./cli/quoted-render.js');
@@ -5566,34 +5998,33 @@ function argValues(args, ...flags) {
5566
5998
  }
5567
5999
  return out;
5568
6000
  }
5569
- function withCustomCardMentionFooter(card, mentionOpenIds, sentToLabel) {
6001
+ function withCustomCardMentionFooter(card, mentionOpenIds, locale) {
5570
6002
  if (mentionOpenIds.length === 0)
5571
6003
  return { ok: true, card };
5572
- const cloned = JSON.parse(JSON.stringify(card));
5573
- const body = cloned.body;
5574
- if (!body || !Array.isArray(body.elements)) {
6004
+ const deduped = [...new Set(mentionOpenIds.filter(Boolean))];
6005
+ const cloned = appendReplyCardFooterToV2Card(card, {
6006
+ brand: '',
6007
+ recipientOpenIds: deduped,
6008
+ locale,
6009
+ });
6010
+ if (!cloned) {
5575
6011
  return {
5576
6012
  ok: false,
5577
- error: '自定义卡片带 --mention/--mention-back 时必须是 schema 2.0 且包含 body.elements;或改用 --no-mention 并在卡片 JSON 内自行处理展示',
6013
+ error: '自定义卡片带 --mention/--mention-back 时必须是 schema 2.0、包含 body.elements,且未占用 botmux_reply_footer 元素 ID;或改用 --no-mention 并在卡片 JSON 内自行处理展示',
5578
6014
  };
5579
6015
  }
5580
- const deduped = [...new Set(mentionOpenIds.filter(Boolean))];
5581
- body.elements.push({ tag: 'hr' }, {
5582
- tag: 'markdown',
5583
- text_size: 'notation_small_v2',
5584
- content: `<font color='grey'>${sentToLabel}${deduped.map(id => `<at id=${id}></at>`).join(' ')}</font>`,
5585
- });
5586
6016
  return { ok: true, card: cloned };
5587
6017
  }
5588
6018
  // Card v2 body builder helpers — extracted to im/lark/md-card.ts so the
5589
6019
  // daemon's bridge fallback path can produce identical cards. cmdSend
5590
6020
  // keeps using `buildImageCardElements` from there.
5591
- import { buildImageCardElements, brandFooterSegment, prepareCardMarkdown } from './im/lark/md-card.js';
6021
+ import { appendReplyCardFooterToV2Card, buildImageCardElements, buildReplyCardFooter, prepareCardMarkdown, } from './im/lark/md-card.js';
5592
6022
  import { applyInlineMentions } from './im/lark/inline-mentions.js';
5593
6023
  import { renderBrandTemplate } from './im/lark/brand-template.js';
5594
- import { resolveBrandLabel } from './bot-registry.js';
6024
+ import { resolveBrandLabel, resolveUsageDisplay } from './bot-registry.js';
5595
6025
  import { config } from './config.js';
5596
- import { resolveQuoteTarget, validateMentionDecision, parseAttentionFlag, attentionUsageError, managedVcQuoteError, managedVcCustomCardError, managedVcSendControlError, managedVcSendPayloadError, containsLarkAtTag, } from './services/send-policy.js';
6026
+ import { getSessionUsageSnapshot } from './core/cost-calculator.js';
6027
+ import { resolveQuoteTarget, validateMentionDecision, shouldBlockMentionBackByParticipants, parseAttentionFlag, attentionUsageError, managedVcQuoteError, managedVcCustomCardError, managedVcSendControlError, managedVcSendPayloadError, containsLarkAtTag, } from './services/send-policy.js';
5597
6028
  /**
5598
6029
  * Sandbox relay mode for `botmux send`. Inside a file-sandbox the CLI cannot
5599
6030
  * read bots.json or reach Lark directly (creds are deliberately absent), so we
@@ -5759,6 +6190,98 @@ async function relaySend(rest, relayDir) {
5759
6190
  console.error('relay: 等待 daemon 投递超时(120s)');
5760
6191
  process.exit(1);
5761
6192
  }
6193
+ /** True if the running bot (by daemon-injected larkAppId) is core-only
6194
+ * (apiOnly). Read-only + never throws: used by `botmux send` to refuse early
6195
+ * with a clear message. Reads bots.json best-effort; an apiOnly bot runs
6196
+ * non-isolated (no Feishu secret to protect), so bots.json is readable here. */
6197
+ function currentBotIsApiOnly(larkAppId) {
6198
+ try {
6199
+ return loadBotsJson().some((b) => b?.larkAppId === larkAppId && b?.apiOnly === true);
6200
+ }
6201
+ catch {
6202
+ return false;
6203
+ }
6204
+ }
6205
+ /** Central CLI session-transport capability check. A turn has NO Feishu
6206
+ * transport when either the running bot is core-only (apiOnly) OR the turn runs
6207
+ * in an HTTP virtual session (BOTMUX_CHAT_ID starts with http_async_ or
6208
+ * http_wait_). This is the single source of truth every Feishu-touching CLI
6209
+ * command consults — send/dispatch (writes) AND history/quoted/bots (reads) —
6210
+ * so a normal bot in a virtual session can't reach Feishu by reloading
6211
+ * bots.json for a real client (the daemon side is already gated by
6212
+ * larkTransportEnabled; this closes the non-sandbox local-CLI path). Kept
6213
+ * read-only and total (never throws). */
6214
+ function currentTurnHasNoTransport() {
6215
+ const chatId = process.env.BOTMUX_CHAT_ID ?? '';
6216
+ if (chatId.startsWith('http_async_') || chatId.startsWith('http_wait_'))
6217
+ return true;
6218
+ const appId = process.env.BOTMUX_LARK_APP_ID;
6219
+ return !!appId && currentBotIsApiOnly(appId);
6220
+ }
6221
+ /** Tamper-resistant managed-origin transport check for the root-dispatch gate.
6222
+ * Resolves the origin session via the pid-marker ANCESTRY (process.ppid walk),
6223
+ * NOT the mutable BOTMUX_SESSION_ID env, then loads that session's record and
6224
+ * judges transport from its chatId + its bot's apiOnly config. So `env -u
6225
+ * BOTMUX_SESSION_ID -u BOTMUX_CHAT_ID -u BOTMUX_LARK_APP_ID` cannot shed the
6226
+ * managed no-transport identity. Returns false (not gated) when no managed
6227
+ * origin resolves — a bare host-operator shell keeps full access. Total; on any
6228
+ * resolution error falls back to the env-based check (never throws). */
6229
+ function managedOriginHasNoTransport() {
6230
+ try {
6231
+ const ctx = resolveSessionContext(resolveDataDir(), process.env.BOTMUX_SESSION_ID);
6232
+ if (!ctx?.sessionId) {
6233
+ // No managed origin at all (bare operator). Still honor an explicit env
6234
+ // signal if present (daemon-spawned turn whose marker was pruned), but a
6235
+ // truly bare shell has neither → not gated.
6236
+ return !!process.env.BOTMUX_SESSION_ID && currentTurnHasNoTransport();
6237
+ }
6238
+ const s = loadSessions().get(ctx.sessionId);
6239
+ if (!s) {
6240
+ // Marker resolved a session id but no record on disk (riff sandbox etc.) —
6241
+ // fall back to the env view for that same managed turn.
6242
+ return currentTurnHasNoTransport();
6243
+ }
6244
+ const chatId = s.chatId ?? '';
6245
+ if (chatId.startsWith('http_async_') || chatId.startsWith('http_wait_'))
6246
+ return true;
6247
+ return !!s.larkAppId && currentBotIsApiOnly(s.larkAppId);
6248
+ }
6249
+ catch {
6250
+ return !!process.env.BOTMUX_SESSION_ID && currentTurnHasNoTransport();
6251
+ }
6252
+ }
6253
+ /** Refuse a Feishu-touching CLI command for a no-transport turn with a clear,
6254
+ * actionable message (not a deep client error), then exit. `op` names the
6255
+ * command for the message. Returns true if it refused+exited is imminent — but
6256
+ * it calls process.exit(2), so callers just `if (assertTurnTransportOrExit(...)) return;`
6257
+ * for type-flow clarity; execution never continues past the exit. */
6258
+ function assertTurnTransportOrExit(op) {
6259
+ if (!currentTurnHasNoTransport())
6260
+ return;
6261
+ const chatId = process.env.BOTMUX_CHAT_ID ?? '';
6262
+ const why = chatId.startsWith('http_async_') || chatId.startsWith('http_wait_')
6263
+ ? 'this turn runs in an HTTP control-API session (no Feishu chat)'
6264
+ : 'this is a core-only (apiOnly) bot with no Feishu connection';
6265
+ console.error(`botmux ${op} is unavailable: ${why}.\n` +
6266
+ `Feishu read/write is not possible here — the turn communicates only over the HTTP\n` +
6267
+ `control API (input via trigger, output via trigger-result). Produce your normal answer.`);
6268
+ process.exit(2);
6269
+ }
6270
+ /** Target-aware variant: gate on the RESOLVED session (its chatId + owning bot's
6271
+ * apiOnly), not just the calling process env. The env gate can't see a
6272
+ * `--session-id <other>` argument that targets a different (virtual) session, so
6273
+ * commands that accept --session-id must call this AFTER resolving the target to
6274
+ * close the cross-session bypass. Read-only + total (never throws besides exit). */
6275
+ function assertSessionTransportOrExit(session, op) {
6276
+ const chatId = session.chatId ?? '';
6277
+ const virtual = chatId.startsWith('http_async_') || chatId.startsWith('http_wait_');
6278
+ const apiOnly = !!session.larkAppId && currentBotIsApiOnly(session.larkAppId);
6279
+ if (!virtual && !apiOnly)
6280
+ return;
6281
+ console.error(`botmux ${op} is unavailable for the target session: ${virtual ? 'it is an HTTP control-API session (no Feishu chat)' : 'its bot is core-only (apiOnly)'}.\n` +
6282
+ `Feishu read/write is not possible for that session.`);
6283
+ process.exit(2);
6284
+ }
5762
6285
  /** Under read isolation the CLI is denied bots.json, so `loadBotConfigs()` reads
5763
6286
  * nothing. The worker instead wrote THIS bot's own secret to a per-bot cred file
5764
6287
  * (its own is readable; siblings' are denied). Register just this bot from that
@@ -5780,14 +6303,24 @@ async function registerSelfFromCredFile() {
5780
6303
  catch {
5781
6304
  return; // no cred file → not isolated (or first layer supplies creds elsewhere)
5782
6305
  }
5783
- if (!cred.larkAppSecret)
6306
+ // apiOnly bots legitimately have an empty secret — don't bail on that, but DO
6307
+ // carry the apiOnly flag through so the reconstructed config keeps the
6308
+ // transport boundary (getBotClient throws for apiOnly). A non-apiOnly bot with
6309
+ // no secret is still a no-op (nothing to register).
6310
+ if (!cred.larkAppSecret && cred.apiOnly !== true)
5784
6311
  return;
5785
6312
  const { registerBot } = await import('./bot-registry.js');
5786
6313
  registerBot({
5787
6314
  larkAppId: appId,
5788
- larkAppSecret: cred.larkAppSecret,
6315
+ larkAppSecret: cred.larkAppSecret ?? '',
6316
+ apiOnly: cred.apiOnly === true || undefined,
5789
6317
  cliId: 'claude-code',
5790
6318
  brand: cred.brand,
6319
+ usageDisplay: process.env.BOTMUX_USAGE_DISPLAY === 'streaming' ||
6320
+ process.env.BOTMUX_USAGE_DISPLAY === 'footer' ||
6321
+ process.env.BOTMUX_USAGE_DISPLAY === 'off'
6322
+ ? process.env.BOTMUX_USAGE_DISPLAY
6323
+ : undefined,
5791
6324
  });
5792
6325
  }
5793
6326
  /**
@@ -5846,9 +6379,15 @@ function riffModeSession(opts = {}) {
5846
6379
  const botConfig = {
5847
6380
  larkAppId: appId,
5848
6381
  larkAppSecret: appSecret,
6382
+ apiOnly: process.env.BOTMUX_API_ONLY === '1' || undefined,
5849
6383
  brand,
5850
6384
  cliId: 'riff',
5851
6385
  allowedUsers: [],
6386
+ usageDisplay: process.env.BOTMUX_USAGE_DISPLAY === 'streaming' ||
6387
+ process.env.BOTMUX_USAGE_DISPLAY === 'footer' ||
6388
+ process.env.BOTMUX_USAGE_DISPLAY === 'off'
6389
+ ? process.env.BOTMUX_USAGE_DISPLAY
6390
+ : undefined,
5852
6391
  };
5853
6392
  const session = {
5854
6393
  sessionId,
@@ -5893,6 +6432,10 @@ async function cmdSend(rest) {
5893
6432
  `chat-facing side effects belong in a hostExecutor activity, not a subagent.`);
5894
6433
  process.exit(2);
5895
6434
  }
6435
+ // No-transport turn (apiOnly bot OR HTTP virtual session): refuse via the
6436
+ // central session-capability gate — same hard door every Feishu-touching CLI
6437
+ // command consults.
6438
+ assertTurnTransportOrExit('send');
5896
6439
  // Managed output attribution must come from the live process-tree marker,
5897
6440
  // whose turn/attempt is refreshed by the worker. BOTMUX_TURN_ID is a
5898
6441
  // spawn-time fallback and can be stale in a detached child after a later
@@ -6146,6 +6689,11 @@ async function cmdSend(rest) {
6146
6689
  console.error(`session ${sid} 缺少 larkAppId`);
6147
6690
  process.exit(1);
6148
6691
  }
6692
+ // Target-aware gate on the RESOLVED source session: `send --session-id <virtual>`
6693
+ // (or an apiOnly bot's session) must be refused even if the ambient env looks
6694
+ // transport-capable, and regardless of any `--chat-id` override — a no-transport
6695
+ // turn may not originate ANY Feishu write. Closes the env-only gap for send.
6696
+ assertSessionTransportOrExit({ chatId: s.chatId, larkAppId: s.larkAppId }, 'send');
6149
6697
  let deferredMaterializedByThisCommand = false;
6150
6698
  let deferredTopicRootMessageIdForOutput;
6151
6699
  // 本轮的回复锚点:优先按 turnId 精确取 per-turn 落点(排队/并发轮次不再被
@@ -6349,7 +6897,11 @@ async function cmdSend(rest) {
6349
6897
  const markerDir = join(resolveDataDir(), 'turn-sends');
6350
6898
  if (!existsSync(markerDir))
6351
6899
  mkdirSync(markerDir, { recursive: true });
6352
- appendFileSync(join(markerDir, `${sid}.jsonl`), JSON.stringify({ sentAtMs, messageId }) + '\n');
6900
+ const marker = { sentAtMs, messageId };
6901
+ const previewText = buildBridgeSendPreviewText(content);
6902
+ if (previewText)
6903
+ marker.previewText = previewText;
6904
+ appendFileSync(join(markerDir, `${sid}.jsonl`), JSON.stringify(marker) + '\n');
6353
6905
  }
6354
6906
  catch { /* best-effort:漏记只多一条兜底,不致命 */ }
6355
6907
  }
@@ -6435,7 +6987,15 @@ async function cmdSend(rest) {
6435
6987
  const markerDir = join(resolveDataDir(), 'turn-sends');
6436
6988
  if (!existsSync(markerDir))
6437
6989
  mkdirSync(markerDir, { recursive: true });
6438
- appendFileSync(join(markerDir, `${sid}.jsonl`), JSON.stringify({ sentAtMs: Date.now(), messageId: `doc:${docTarget.commentId}`, contentLength: content.length }) + '\n');
6990
+ const marker = {
6991
+ sentAtMs: Date.now(),
6992
+ messageId: `doc:${docTarget.commentId}`,
6993
+ contentLength: content.length,
6994
+ };
6995
+ const previewText = buildBridgeSendPreviewText(content);
6996
+ if (previewText)
6997
+ marker.previewText = previewText;
6998
+ appendFileSync(join(markerDir, `${sid}.jsonl`), JSON.stringify(marker) + '\n');
6439
6999
  }
6440
7000
  catch { /* best-effort:漏记只多一条兜底 */ }
6441
7001
  // 清理已消费的 per-turn 落点,避免 session 文件无限堆积。
@@ -6486,6 +7046,52 @@ async function cmdSend(rest) {
6486
7046
  console.error(mentionGate.error);
6487
7047
  process.exit(2);
6488
7048
  }
7049
+ // Register bots so the Lark client works. MUST run before the participant
7050
+ // gate below: that gate calls getGroupStats → getBotClient, which throws
7051
+ // "Bot not registered" in this standalone `botmux send` process (a fresh
7052
+ // process with an empty registry) — getGroupStats would then soft-fail to
7053
+ // {999,999} and wrongly block --mention-back even in a true 1v1. registerBot
7054
+ // is idempotent, so the downstream send path reuses these same clients.
7055
+ // envPinnedRiffBot is re-registered LAST so a remote env credential is never
7056
+ // clobbered by a stale bots.json entry for the same app.
7057
+ const { registerBot, loadBotConfigs, findOncallChatForAnyBot } = await import('./bot-registry.js');
7058
+ try {
7059
+ for (const cfg of loadBotConfigs())
7060
+ registerBot(cfg);
7061
+ }
7062
+ catch { /* */ }
7063
+ if (envPinnedRiffBot) {
7064
+ try {
7065
+ registerBot(envPinnedRiffBot);
7066
+ }
7067
+ catch { /* */ }
7068
+ }
7069
+ // Participant gate for --mention-back: in a true 1v1 the triggerer is the
7070
+ // only counterpart so auto-@-ing them back is unambiguous, but once a third
7071
+ // party joins (humans + bots > 2) "who triggered this turn" is no longer
7072
+ // reliably "who should be addressed". Force an explicit --mention there.
7073
+ // Symmetric with the inbound un-@ gate (event-dispatcher getGroupStats).
7074
+ // Only fetch when mention-back is actually requested AND the chat isn't a p2p
7075
+ // DM (inherently 1v1) — keeps the common send path free of an API round-trip.
7076
+ if (mentionBack && s.chatType !== 'p2p' && s.larkAppId && s.chatId && !sendTopLevel) {
7077
+ try {
7078
+ const { getGroupStats } = await import('./im/lark/event-dispatcher.js');
7079
+ const { userCount, botCount } = await getGroupStats(s.larkAppId, s.chatId);
7080
+ if (shouldBlockMentionBackByParticipants({ chatType: s.chatType, userCount, botCount })) {
7081
+ console.error(`--mention-back 在多人会话(当前 ${userCount} 人 + ${botCount} bot)里不可用:`
7082
+ + '"回复触发这轮的人/bot" 在多方场景可能 @ 错对象。请改用 --mention <ou:Name> 显式点名,'
7083
+ + '或 --no-mention 不 @。');
7084
+ process.exit(2);
7085
+ }
7086
+ }
7087
+ catch (err) {
7088
+ // getGroupStats already soft-fails to {999,999} (→ block) internally, so
7089
+ // reaching here means the dynamic import itself failed. Fail-closed to a
7090
+ // clear error rather than silently letting a possibly-wrong @ through.
7091
+ console.error(`无法确认会话人数以校验 --mention-back:${err?.message ?? err}。请改用 --mention <ou:Name> 或 --no-mention。`);
7092
+ process.exit(2);
7093
+ }
7094
+ }
6489
7095
  // --mention-back: @ the sender of the message this turn is replying to
6490
7096
  // (open_id from the session — model needn't know it). Bare-name form so it
6491
7097
  // renders as a trailing <at>.
@@ -6506,19 +7112,6 @@ async function cmdSend(rest) {
6506
7112
  process.exit(1);
6507
7113
  }
6508
7114
  }
6509
- // Register bots so Lark client works
6510
- const { registerBot, loadBotConfigs, findOncallChatForAnyBot } = await import('./bot-registry.js');
6511
- try {
6512
- for (const cfg of loadBotConfigs())
6513
- registerBot(cfg);
6514
- }
6515
- catch { /* */ }
6516
- if (envPinnedRiffBot) {
6517
- try {
6518
- registerBot(envPinnedRiffBot);
6519
- }
6520
- catch { /* */ }
6521
- }
6522
7115
  const { sendMessage, replyMessage, uploadImage, uploadFile, MessageWithdrawnError } = await import('./im/lark/client.js');
6523
7116
  const appId = s.larkAppId;
6524
7117
  // Effective target chat for top-level mode (defaults to session's chat)
@@ -6864,7 +7457,7 @@ async function cmdSend(rest) {
6864
7457
  cc: footerAddressing.cc,
6865
7458
  inlinedIds: [],
6866
7459
  });
6867
- const withFooter = withCustomCardMentionFooter(customCard, mentionFooter, t('card.sent_to', undefined, localeForBot(appId)));
7460
+ const withFooter = withCustomCardMentionFooter(customCard, mentionFooter, localeForBot(appId));
6868
7461
  if (!withFooter.ok) {
6869
7462
  console.error(`botmux send: ${withFooter.error}`);
6870
7463
  process.exit(2);
@@ -6953,10 +7546,6 @@ async function cmdSend(rest) {
6953
7546
  // Brand segment honours this bot's configured brandLabel (unset →
6954
7547
  // default botmux, '' → suppressed, else custom). Same resolver/rule as
6955
7548
  // the daemon's card builders so both send paths render identically.
6956
- const footerParts = [];
6957
- const brandSeg = brandFooterSegment(renderBrandTemplate(resolveBrandLabel(appId), s.workingDir));
6958
- if (brandSeg)
6959
- footerParts.push(brandSeg);
6960
7549
  // All real mentions land on one footer line: human addressee first, then
6961
7550
  // explicit @ targets (incl. handoff bots), then cc. Ids already inlined in
6962
7551
  // the body prose are skipped. Top-level publish keeps sendTo empty.
@@ -6966,9 +7555,13 @@ async function cmdSend(rest) {
6966
7555
  cc: footerAddressing.cc,
6967
7556
  inlinedIds: usedIds,
6968
7557
  });
6969
- if (footerRecipients.length > 0) {
6970
- footerParts.push(`${t('card.sent_to', undefined, localeForBot(appId))}${footerRecipients.map(id => `<at id=${id}></at>`).join(' ')}`);
6971
- }
7558
+ const usageSnapshot = await readCardUsageSnapshotForSend(s, appId);
7559
+ const footer = buildReplyCardFooter({
7560
+ brand: renderBrandTemplate(resolveBrandLabel(appId), s.workingDir),
7561
+ recipientOpenIds: footerRecipients,
7562
+ usage: usageSnapshot,
7563
+ locale: localeForBot(appId),
7564
+ });
6972
7565
  // Footer line (brand 个性签名 + 发送给) and the optional 🔊 语音总结 button
6973
7566
  // share ONE row: footer text on the left (weighted, fills), button pinned
6974
7567
  // to the far right (auto width). When voice isn't configured the footer
@@ -6985,9 +7578,7 @@ async function cmdSend(rest) {
6985
7578
  }
6986
7579
  catch { /* voice module/config unavailable → no button */ }
6987
7580
  }
6988
- const footerContent = footerParts.length > 0
6989
- ? `<font color='grey'>${footerParts.join(' · ')}</font>`
6990
- : '';
7581
+ const footerContent = footer?.content ?? '';
6991
7582
  if (footerContent || voiceOn) {
6992
7583
  elements.push({ tag: 'hr' });
6993
7584
  if (voiceOn) {
@@ -6999,7 +7590,11 @@ async function cmdSend(rest) {
6999
7590
  columns: [
7000
7591
  {
7001
7592
  tag: 'column', width: 'weighted', weight: 1, vertical_align: 'center',
7002
- elements: [{ tag: 'markdown', text_size: 'notation_small_v2', content: footerContent || ' ' }],
7593
+ elements: [footer?.element ?? {
7594
+ tag: 'markdown',
7595
+ text_size: 'notation_small_v2',
7596
+ content: ' ',
7597
+ }],
7003
7598
  },
7004
7599
  {
7005
7600
  tag: 'column', width: 'auto', vertical_align: 'center',
@@ -7017,11 +7612,8 @@ async function cmdSend(rest) {
7017
7612
  });
7018
7613
  }
7019
7614
  else {
7020
- elements.push({
7021
- tag: 'markdown',
7022
- text_size: 'notation_small_v2',
7023
- content: footerContent,
7024
- });
7615
+ if (footer)
7616
+ elements.push(footer.element);
7025
7617
  }
7026
7618
  }
7027
7619
  const cardJson = JSON.stringify({
@@ -7168,6 +7760,9 @@ async function cmdDispatch(rest) {
7168
7760
  --session-id <id> 指定来源会话(默认自动推断)`);
7169
7761
  return;
7170
7762
  }
7763
+ // dispatch opens a real Feishu topic + pulls bots into a chat (a write). A
7764
+ // no-transport turn has no Feishu chat to dispatch into — central hard gate.
7765
+ assertTurnTransportOrExit('dispatch');
7171
7766
  process.env.SESSION_DATA_DIR ??= resolveDataDir();
7172
7767
  const sessionIdArg = argValue(rest, '--session-id');
7173
7768
  const title = argValue(rest, '--title') ?? '';
@@ -7234,6 +7829,11 @@ async function cmdDispatch(rest) {
7234
7829
  console.error(`session ${sid} 缺少 larkAppId`);
7235
7830
  process.exit(1);
7236
7831
  }
7832
+ // Target-aware gate on the RESOLVED source session: dispatch from a virtual /
7833
+ // apiOnly source turn is refused even with a real --chat-id override (a
7834
+ // no-transport turn may not originate a Feishu topic/write). Closes the
7835
+ // `dispatch --session-id <virtual> --chat-id oc_real` env-only gap.
7836
+ assertSessionTransportOrExit({ chatId: s.chatId, larkAppId: s.larkAppId }, 'dispatch');
7237
7837
  const targetChatId = overrideChatId ?? s.chatId;
7238
7838
  if (!targetChatId) {
7239
7839
  console.error(`session ${sid} 缺少 chatId,且未提供 --chat-id`);
@@ -7799,6 +8399,9 @@ botmux create-group — 用一组机器人新建飞书群
7799
8399
  `);
7800
8400
  return;
7801
8401
  }
8402
+ // create-group builds a real Feishu group (cross-bot). A no-transport turn
8403
+ // (apiOnly bot or HTTP virtual session) may not originate one — central gate.
8404
+ assertTurnTransportOrExit('create-group');
7802
8405
  process.env.SESSION_DATA_DIR ??= resolveDataDir();
7803
8406
  const botRefs = argValues(rest, '--bot');
7804
8407
  const name = argValue(rest, '--name');
@@ -8422,10 +9025,22 @@ async function cmdBots(sub, rest) {
8422
9025
  console.error('用法: botmux bots list [--session-id ID]');
8423
9026
  process.exit(1);
8424
9027
  }
9028
+ // `bots list` reads the Feishu chat roster (listChatBotMembers). A no-transport
9029
+ // turn has no chat roster — central hard gate (also stops the routing prompt
9030
+ // from advertising a Feishu-dependent helper in this context).
9031
+ assertTurnTransportOrExit('bots list');
8425
9032
  const sessionIdArg = argValue(rest, '--session-id');
8426
- // 与 history/quoted 同一前奏:本地会话解析 + riff sandbox env 合成会话兜底
8427
- //(远端沙箱无 sessions.json/bots.json `botmux bots list` 照常可用)。
9033
+ // 与 history/quoted 同一前奏:先从本 bot 自己的 send-cred 文件注册,让 Lark client
9034
+ // 在**不读被 deny 的 bots.json** 的前提下可用,再做会话解析 + riff sandbox env
9035
+ // 合成会话兜底。漏掉 registerSelfFromCredFile() 时读隔离 bot 的
9036
+ // `botmux bots list` 会在 getBotClient() 上抛 "Bot not registered",
9037
+ // listChatBotMembers() 把它降级成 legacy discovery(configured 行同样来自
9038
+ // bots.json)→ 返回 `total: 0`。**失败形态是静默的**:沙盒 bot 拿到空花名册会
9039
+ // 按"只 @ mentionable 的"判定群里没人可 @,多 bot 协作直接不发生且不报错。
9040
+ await registerSelfFromCredFile();
8428
9041
  const { sid, larkAppId: resolvedAppId, session: s } = await resolveSessionAppId(sessionIdArg);
9042
+ // Target-aware gate (see cmdHistory).
9043
+ assertSessionTransportOrExit({ chatId: s.chatId, larkAppId: resolvedAppId }, 'bots list');
8429
9044
  const appId = resolvedAppId;
8430
9045
  const dataDir = resolveDataDir();
8431
9046
  const botInfoPath = join(dataDir, 'bots-info.json');
@@ -9495,6 +10110,29 @@ async function runPluginCommandByName(rawCommand, commandArgs) {
9495
10110
  process.exitCode = result;
9496
10111
  return true;
9497
10112
  }
10113
+ // ─── Central root-dispatch transport gate ──────────────────────────────────
10114
+ // A MANAGED no-transport turn (a CLI turn the daemon spawned for an apiOnly bot
10115
+ // or an HTTP virtual session) must not run ANY Lark-facing command. The managed
10116
+ // origin is resolved via the pid-marker ANCESTRY (resolveSessionContext walks
10117
+ // process.ppid to a worker-written marker) — NOT the mutable BOTMUX_SESSION_ID
10118
+ // env — so `env -u BOTMUX_SESSION_ID … botmux create-group` cannot shed the
10119
+ // managed identity. We then load THAT session's record and gate on its chatId +
10120
+ // its bot's apiOnly (config, not env), so unsetting BOTMUX_CHAT_ID/LARK_APP_ID
10121
+ // also can't flip the verdict. Covers every Feishu-facing verb by construction.
10122
+ // A BARE host-operator shell (no ancestry marker, no env session) resolves no
10123
+ // managed origin → NOT gated: the operator keeps full access. per-command +
10124
+ // daemon-side getBotClient/larkTransportEnabled gates remain authoritative.
10125
+ const LARK_FACING_COMMANDS = new Set([
10126
+ 'send', 'dispatch', 'create-group', 'history', 'quoted', 'bots', 'grant', 'react', 'thread',
10127
+ 'vc-agent', 'report',
10128
+ ]);
10129
+ if (LARK_FACING_COMMANDS.has(command) && managedOriginHasNoTransport()) {
10130
+ console.error(`botmux ${command} is unavailable: this managed turn has no Feishu transport ` +
10131
+ `(core-only apiOnly bot or HTTP control-API session).\n` +
10132
+ `Feishu read/write is not possible for this turn — it communicates only over the HTTP\n` +
10133
+ `control API (input via trigger, output via trigger-result). Produce your normal answer.`);
10134
+ process.exit(2);
10135
+ }
9498
10136
  switch (command) {
9499
10137
  case '--version':
9500
10138
  case '-v':
@@ -9524,6 +10162,9 @@ switch (command) {
9524
10162
  case 'start':
9525
10163
  await cmdStart();
9526
10164
  break;
10165
+ case 'serve':
10166
+ await cmdServe(process.argv.slice(3));
10167
+ break;
9527
10168
  case 'start-bot':
9528
10169
  await cmdStartBot(process.argv.slice(3));
9529
10170
  break;
@@ -9564,6 +10205,9 @@ switch (command) {
9564
10205
  case 'ls':
9565
10206
  await cmdList();
9566
10207
  break;
10208
+ case '__zmx-attach-managed':
10209
+ cmdManagedZmxAttach(process.argv.slice(3));
10210
+ break;
9567
10211
  case 'delete':
9568
10212
  case 'del':
9569
10213
  case 'rm':
@@ -9642,7 +10286,9 @@ switch (command) {
9642
10286
  process.exitCode = 2;
9643
10287
  break;
9644
10288
  }
9645
- process.env.SESSION_DATA_DIR ??= resolveDataDir();
10289
+ if (!process.env.SESSION_DATA_DIR?.trim()) {
10290
+ process.env.SESSION_DATA_DIR = resolveDataDir();
10291
+ }
9646
10292
  const { runMcpGateway } = await import('./core/plugins/mcp/gateway.js');
9647
10293
  await runMcpGateway();
9648
10294
  break;
@@ -9747,6 +10393,9 @@ switch (command) {
9747
10393
  case 'send':
9748
10394
  await cmdSend(process.argv.slice(3));
9749
10395
  break;
10396
+ case 'chat':
10397
+ await cmdChat(process.argv.slice(3));
10398
+ break;
9750
10399
  case 'dispatch':
9751
10400
  await cmdDispatch(process.argv.slice(3));
9752
10401
  break;