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
@@ -9,6 +9,7 @@ import { V3_SESSION_RUN_MUTATION_ROUTE_PREFIX } from '../workflows/v3/session-re
9
9
  import { listenWithProbe } from '../utils/listen-with-probe.js';
10
10
  import { dashboardSecretPath } from './dashboard-secret.js';
11
11
  import * as sessionStore from '../services/session-store.js';
12
+ import { cliSupportsNativeUsage } from '../services/transcript-resolver.js';
12
13
  import * as asyncTriggerStore from '../services/async-trigger-store.js';
13
14
  import { resolveAsyncTriggerState, decideAsyncOwnership } from '../services/async-trigger-state.js';
14
15
  import * as scheduleStore from '../services/schedule-store.js';
@@ -68,11 +69,12 @@ import { readGlobalConfig } from '../global-config.js';
68
69
  import { normalizeChatReplyMode, setChatReplyMode } from '../services/chat-reply-mode-store.js';
69
70
  import * as chatFirstSeenStore from '../services/chat-first-seen-store.js';
70
71
  import * as scheduler from './scheduler.js';
71
- import { listActiveSessions, findActiveBySessionId, closeSession, getActiveSessionsRegistry, transferSession, deliverWriteLinkCardToOwners, forkWorker, suspendWorker, killWorker } from './worker-pool.js';
72
+ import { listActiveSessions, findActiveBySessionId, closeSession, getActiveSessionsRegistry, transferSession, deliverWriteLinkCardToOwners, forkWorker, suspendWorker, killWorker, latestPerBotEnvForRestart, getDaemonReplyCardUsageSnapshot, sessionSupportsWebTerminal, sendWorkerSessionInput, isSessionTransferring } from './worker-pool.js';
72
73
  import { listOnlineDaemons } from '../utils/daemon-discovery.js';
73
74
  import { isSessionStopped } from './session-liveness.js';
74
- import { getChatMode, replyMessage, sendMessage, resolveUnionIdFromOpenId, listThreadMessages, listChatMessages, listChatBotMembers, getUserProfile, getUserProfileStrict, resolveAllowedUsersWithMap } from '../im/lark/client.js';
75
- import { parseApiMessage, cardContentHasUpgradeFallback, resolveMergedCardContent } from '../im/lark/message-parser.js';
75
+ import { isSuspendableBackendType } from './persistent-backend.js';
76
+ import { getChatMode, replyMessage, sendMessage, resolveUnionIdFromOpenId, listThreadMessages, listChatMessages, listChatMessagesUntil, listChatBotMembers, getUserProfile, getUserProfileStrict, resolveAllowedUsersWithMap } from '../im/lark/client.js';
77
+ import { parseApiMessage, cardContentHasUpgradeFallback, resolveMergedCardContent, messageMentionsBot } from '../im/lark/message-parser.js';
76
78
  import { resumeSession, spawnDashboardSession, activateQueuedSession, closeCliMismatchedSessionsForBot, suspendActiveSessionsForBot } from './session-manager.js';
77
79
  import { parseSpawnRequest } from './session-create.js';
78
80
  import { cleanupMaterializedDashboardImages, materializeDashboardImages } from './dashboard-images.js';
@@ -89,6 +91,11 @@ import { resolveCliSelection, selectionKeyForBot } from '../setup/cli-selection.
89
91
  import { checkCliAvailability } from '../setup/cli-availability.js';
90
92
  import { enrichHistorySenders } from '../dashboard/history-senders.js';
91
93
  import { listPendingAsks, submitAskFromDesktop } from './ask-broker.js';
94
+ import { getMessageListenerConfig, sanitizeMessageListenerUpdate, updateMessageListenerConfig, validateMessageListenerUpdate } from '../services/message-listener-store.js';
95
+ import { MAX_MESSAGE_LISTENER_PROMPT_BYTES, normalizeMessageListenerPreviewLimit, previewMessageListenerMatches, buildListenerBotAppIdToOpenId, collectListenerBotAppIds, renderMessageListenerInstruction, } from '../services/message-listener.js';
96
+ import { createMessageListenerRunPreview, createMessageListenerRunPreviewTurnId, getMessageListenerRunPreview, markMessageListenerRunPreviewFailed, markMessageListenerRunPreviewTriggered, } from '../services/message-listener-run-preview-store.js';
97
+ import { listChatMemberDisplays } from '../services/groups-store.js';
98
+ const MESSAGE_LISTENER_PREVIEW_WINDOW_MS = 24 * 60 * 60 * 1000;
92
99
  let exactChatGrantHandler = applyExactChatGrantRequest;
93
100
  /** Test seam: replace the exact-grant service without touching live Feishu/config state. */
94
101
  export function setExactChatGrantHandler(handler) {
@@ -102,16 +109,17 @@ let botAvatarChanger = null;
102
109
  export function setBotAvatarChanger(fn) {
103
110
  botAvatarChanger = fn;
104
111
  }
105
- import { composeRowFromActive, composeRowFromClosed, feishuChatLink, setBotName as setRowsBotName, getBotName, } from './dashboard-rows.js';
106
- import { getBotBrand, getBot, loadBotConfigs, readBotSkillPolicy, getBotTuiSlashAllow } from '../bot-registry.js';
112
+ import { composeRowFromActive, composeRowFromClosed, composeRowFromPersistedActive, feishuChatLink, setBotName as setRowsBotName, getBotName, } from './dashboard-rows.js';
113
+ import { getBotBrand, getBot, getBotOpenId, loadBotConfigs, readBotSkillPolicy, getBotTuiSlashAllow } from '../bot-registry.js';
107
114
  import { normalizeKanbanColumn, normalizeKanbanPosition, normalizeSessionTitle } from './session-board.js';
108
115
  import { validateSlashInjection } from './slash-inject.js';
109
116
  import { validateRoleLibraryPath } from './role-library.js';
110
117
  import { repinSessionWorkingDir } from './session-cwd.js';
111
118
  import { authorizeSessionScopedIpc } from './daemon-ipc-session-auth.js';
112
- import { updateSessionTitle } from './session-title.js';
119
+ import { normalizeSessionTitleSource, updateSessionTitle } from './session-title.js';
113
120
  import { requestAgentSessionRename } from './session-rename.js';
114
- import { sessionAnchorId } from './types.js';
121
+ import { ChatRenameCooldown, ChatRenameSerialQueue, normalizeLarkChatName } from './chat-rename.js';
122
+ import { sessionAnchorId, larkTransportEnabled } from './types.js';
115
123
  import { attachSkillPolicy, detachSkillPolicy } from './skills/im-command.js';
116
124
  import { readSkillRegistry } from '../services/skill-registry-store.js';
117
125
  import { commitDeviceIsolationActivation, DEVICE_ISOLATION_COMMIT_PATH, DEVICE_ISOLATION_PREPARE_PATH, DEVICE_ISOLATION_RELEASE_PATH, logDeviceIsolationActivationError, prepareDeviceIsolationActivation, releaseDeviceIsolationActivation, } from './device-isolation-daemon.js';
@@ -318,8 +326,32 @@ function tokenRouteAuthorized(req, bind) {
318
326
  return ipcHmacAuthorized(req, bind);
319
327
  }
320
328
  function routeHasPublicAccess(method, pathname) {
321
- // Liveness contains no data and performs no mutation.
322
- return method === 'GET' && pathname === '/__health';
329
+ // Liveness contains no data and performs no mutation. /healthz is the
330
+ // core-only public alias of /__health (riff's sandbox launcher polls it).
331
+ return method === 'GET' && (pathname === '/__health' || pathname === '/healthz');
332
+ }
333
+ /**
334
+ * Core-only ONLY: the exact riff-facing routes that bypass the trusted-host HMAC
335
+ * when the daemon runs headless in riff's sandbox. Everything else STILL requires
336
+ * the HMAC (codex P1: authRequired:false opened all 96 IPC routes — a co-resident
337
+ * model turn could read/perturb sessions, scheduler, mutations). This is a tight
338
+ * allowlist of drive-my-own-turn + poll-my-own-output surfaces:
339
+ * POST /api/trigger (start a turn)
340
+ * GET /api/sessions/:id/trigger-result (poll final)
341
+ * GET /api/sessions/:id/insight (poll conversation/progress)
342
+ * `/api/asks/answer` is deliberately EXCLUDED — it is askId-keyed with no
343
+ * session/turn binding, so exposing it would let any co-resident turn hijack
344
+ * another pending ask (codex). riff's async main-link needs no awaiting_input;
345
+ * a future clarify path must be a sessionId+interaction-bound endpoint.
346
+ */
347
+ function routeIsCoreOnlyPublic(method, pathname) {
348
+ if (method === 'POST' && pathname === '/api/trigger')
349
+ return true;
350
+ if (method === 'GET') {
351
+ return /^\/api\/sessions\/[^/]+\/trigger-result$/.test(pathname)
352
+ || /^\/api\/sessions\/[^/]+\/insight$/.test(pathname);
353
+ }
354
+ return false;
323
355
  }
324
356
  function routeHasNarrowUntrustedAuth(method, pathname) {
325
357
  // The receiver action endpoint performs its own rotating worker-capability
@@ -340,7 +372,7 @@ function routeHasNarrowUntrustedAuth(method, pathname) {
340
372
  // 该会话的 rotating per-turn
341
373
  // capability 并绑定到 URL 里的 sessionId(同 /api/asks 姿势)——capability 只
342
374
  // 证明「我是这个会话当前这一轮的 CLI」,选不了别的会话。
343
- if (method === 'POST' && /^\/api\/sessions\/[^/]+\/(?:slash|cd|close)$/.test(pathname))
375
+ if (method === 'POST' && /^\/api\/sessions\/[^/]+\/(?:slash|cd|close|chat-rename)$/.test(pathname))
344
376
  return true;
345
377
  if (method === 'POST' && pathname === '/api/hooks/emit')
346
378
  return true;
@@ -378,12 +410,50 @@ function trustedHostAuthorized(req, pathname, port, secret) {
378
410
  ipcRoute('GET', '/__health', (_req, res) => {
379
411
  jsonRes(res, 200, { ok: true });
380
412
  });
381
- export { composeRowFromActive, composeRowFromClosed };
413
+ // Core-only readiness barrier (codex P1-3): the daemon binds its HTTP port BEFORE
414
+ // restoreActiveSessions / v3 cold-attach / scheduler finish, so a launcher that
415
+ // triggers the instant the port answers would race durable restore (transient
416
+ // not_found / re-fire). /healthz returns 503 until the daemon marks itself ready
417
+ // (setCoreOnlyReady, called AFTER restore in daemon.ts). Non-core-only daemons
418
+ // never set this gate, so /healthz stays an unconditional 200 there.
419
+ let coreOnlyReadinessGate = false; // true only in core-only, until ready
420
+ let coreOnlyReady = false;
421
+ export function armCoreOnlyReadinessGate() { coreOnlyReadinessGate = true; }
422
+ export function setCoreOnlyReady() { coreOnlyReady = true; }
423
+ /** @internal test-only: reset the core-only readiness gate between cases. */
424
+ export function __testOnly_resetCoreOnlyReadiness() { coreOnlyReadinessGate = false; coreOnlyReady = false; }
425
+ /** True when the readiness gate is armed (core-only) but restore hasn't finished.
426
+ * The server-level gate returns 503 for the public control routes in this state,
427
+ * and /healthz reports 'starting' — a barrier so riff can't trigger into a racing
428
+ * durable restore even if it skips the healthz probe (codex P1). */
429
+ function coreOnlyNotReady() { return coreOnlyReadinessGate && !coreOnlyReady; }
430
+ // Public alias for core-only: riff's sandbox launcher polls GET /healthz to know
431
+ // the service is FULLY up (bound AND restore-complete). 200 {ok:true} once ready;
432
+ // 503 {ok:false,status:'starting'} while the readiness gate is armed but not ready.
433
+ ipcRoute('GET', '/healthz', (_req, res) => {
434
+ if (coreOnlyNotReady()) {
435
+ return jsonRes(res, 503, { ok: false, status: 'starting' });
436
+ }
437
+ jsonRes(res, 200, { ok: true });
438
+ });
439
+ export { composeRowFromActive, composeRowFromClosed, composeRowFromPersistedActive };
382
440
  // Re-export setBotName for backwards-compatible imports (daemon.ts). Both
383
441
  // callers (this module's cachedBotName + dashboard-rows' cachedBotName) need
384
442
  // to be primed; here we forward to the rows module which is the canonical
385
443
  // holder.
386
444
  export function setBotName(name) { setRowsBotName(name); }
445
+ function composeDashboardSessionRows() {
446
+ const active = listActiveSessions().map(composeRowFromActive);
447
+ const activeIds = new Set(active.map(row => row.sessionId));
448
+ const persisted = sessionStore.listSessions();
449
+ const unregisteredActive = persisted
450
+ .filter(session => session.status === 'active' && !activeIds.has(session.sessionId))
451
+ .map(composeRowFromPersistedActive);
452
+ const closed = persisted
453
+ .filter(session => session.status === 'closed' && !activeIds.has(session.sessionId))
454
+ .map(composeRowFromClosed);
455
+ return [...active, ...unregisteredActive, ...closed];
456
+ }
387
457
  // The daemon's own larkAppId, primed at startup. Required for the groups
388
458
  // endpoints below which proxy calls into groups-store on this bot's behalf.
389
459
  let cachedLarkAppId = '';
@@ -450,23 +520,35 @@ ipcRoute('POST', '/api/asks/answer', async (req, res) => {
450
520
  return jsonRes(res, 200, { ok: true, outcome });
451
521
  });
452
522
  ipcRoute('GET', '/api/sessions', (_req, res) => {
453
- // Active first (live state), closed appended (historical)
454
- const active = listActiveSessions().map(composeRowFromActive);
455
- const activeIds = new Set(active.map(r => r.sessionId));
456
- const closed = sessionStore.listSessions()
457
- .filter(s => s.status === 'closed' && !activeIds.has(s.sessionId))
458
- .map(composeRowFromClosed);
459
- jsonRes(res, 200, { sessions: [...active, ...closed] });
523
+ // Runtime active first, then persisted active rows that restore deliberately
524
+ // left detached, then closed history. Persisted-active must never be projected
525
+ // through composeRowFromClosed: teardown uncertainty is not a close.
526
+ jsonRes(res, 200, { sessions: composeDashboardSessionRows() });
460
527
  });
461
528
  ipcRoute('GET', '/api/sessions/:sessionId', (_req, res, params) => {
462
529
  const ds = findActiveBySessionId(params.sessionId);
463
530
  if (ds)
464
531
  return jsonRes(res, 200, { session: composeRowFromActive(ds) });
465
- const closed = sessionStore.listSessions().find(s => s.sessionId === params.sessionId);
466
- if (closed)
467
- return jsonRes(res, 200, { session: composeRowFromClosed(closed) });
532
+ const persisted = sessionStore.listSessions().find(s => s.sessionId === params.sessionId);
533
+ if (persisted) {
534
+ return jsonRes(res, 200, {
535
+ session: persisted.status === 'active'
536
+ ? composeRowFromPersistedActive(persisted)
537
+ : composeRowFromClosed(persisted),
538
+ });
539
+ }
468
540
  jsonRes(res, 404, { error: 'not_found' });
469
541
  });
542
+ /** Low-frequency card-display read used by `botmux send`. Keeping the
543
+ * transcript reader and per-bot visibility decision in the resident daemon
544
+ * preserves its incremental cache and live config instead of making every
545
+ * short-lived CLI process rescan the Session or guess from sandboxed files. */
546
+ ipcRoute('GET', '/api/sessions/:sessionId/usage', (_req, res, params) => {
547
+ const ds = findActiveBySessionId(params.sessionId);
548
+ if (!ds)
549
+ return jsonRes(res, 404, { error: 'not_found' });
550
+ jsonRes(res, 200, { usage: getDaemonReplyCardUsageSnapshot(ds) });
551
+ });
470
552
  /** Canonical daemon-side close used by the dashboard and `botmux delete`.
471
553
  * Host callers authenticate with HMAC; a read-isolated CLI may close only its
472
554
  * exact live session with the rotating per-turn capability. */
@@ -489,6 +571,11 @@ ipcRoute('POST', '/api/sessions/:sessionId/close', async (req, res, params) => {
489
571
  function postRestartNotice(ds, fresh) {
490
572
  if (!ds.larkAppId)
491
573
  return;
574
+ // No-transport session (apiOnly bot or HTTP virtual chat) has no Feishu chat
575
+ // to post a restart notice into — skip (the raw sendMessage/replyMessage below
576
+ // bypass sessionReply's gate). Best-effort path; a silent skip is correct.
577
+ if (!larkTransportEnabled({ chatId: ds.chatId, apiOnly: getBot(ds.larkAppId).config.apiOnly }))
578
+ return;
492
579
  const loc = localeForBot(ds.larkAppId);
493
580
  const cliName = getCliDisplayName(ds.session.cliId ?? 'claude-code');
494
581
  const text = fresh
@@ -511,6 +598,9 @@ ipcRoute('POST', '/api/sessions/:sessionId/restart', (_req, res, params) => {
511
598
  const ds = findActiveBySessionId(params.sessionId);
512
599
  if (!ds)
513
600
  return jsonRes(res, 404, { ok: false, error: 'session_not_active' });
601
+ if (isSessionTransferring(ds)) {
602
+ return jsonRes(res, 409, { ok: false, error: 'session_transferring' });
603
+ }
514
604
  // Adopt/observed sessions: botmux never owned the CLI — restarting would kill
515
605
  // the user's real tmux/zellij pane. Hard-reject (the worker self-guards too).
516
606
  if (ds.adoptedFrom || ds.initConfig?.adoptMode) {
@@ -519,8 +609,10 @@ ipcRoute('POST', '/api/sessions/:sessionId/restart', (_req, res, params) => {
519
609
  const cliId = ds.session.cliId ?? 'unknown';
520
610
  if (ds.worker && !ds.worker.killed) {
521
611
  // Live worker → in-place CLI restart (kills the CLI, respawns with --resume).
612
+ // 捎带最新 per-bot env:dashboard 改完 env 后重启才真正生效(与 /restart 同逻辑)。
522
613
  try {
523
- ds.worker.send({ type: 'restart' });
614
+ ds.workerReady = false;
615
+ ds.worker.send({ type: 'restart', env: latestPerBotEnvForRestart(ds) });
524
616
  }
525
617
  catch (err) {
526
618
  return jsonRes(res, 502, { ok: false, error: String(err) });
@@ -546,6 +638,9 @@ ipcRoute('POST', '/api/sessions/:sessionId/suspend', (_req, res, params) => {
546
638
  const ds = findActiveBySessionId(params.sessionId);
547
639
  if (!ds)
548
640
  return jsonRes(res, 404, { ok: false, error: 'session_not_active' });
641
+ if (isSessionTransferring(ds)) {
642
+ return jsonRes(res, 409, { ok: false, error: 'session_transferring' });
643
+ }
549
644
  // Adopt/observed sessions: botmux never owned the CLI — suspending would kill
550
645
  // the user's real tmux/zellij pane. Same guard as /restart.
551
646
  if (ds.adoptedFrom || ds.initConfig?.adoptMode) {
@@ -565,11 +660,10 @@ ipcRoute('POST', '/api/sessions/:sessionId/suspend', (_req, res, params) => {
565
660
  });
566
661
  /**
567
662
  * Count host-overload降压 candidates for THIS daemon's scope, so the alert card
568
- * can show "僵尸 N / 闲置 M" before the owner clicks. `stopped` counts zombies
569
- * from the SHARED session store (same answer on every daemon the card handler
570
- * only takes it from one), `idle` counts THIS daemon's own idle live workers
571
- * (owning-daemon-authoritative the handler sums across daemons). Mirrors the
572
- * exact classification the sweep uses so the preview matches what a click does.
663
+ * can show "僵尸 N / 闲置 M" before the owner clicks. Both counts are local to
664
+ * THIS daemon: its session store is bot-scoped, and live workers only exist in
665
+ * their owning process. The card handler sums every daemon's response. Mirrors
666
+ * the exact classification the sweep uses so the preview matches a click.
573
667
  */
574
668
  ipcRoute('GET', '/api/host-overload/counts', (_req, res) => {
575
669
  const stopped = sessionStore.listSessions().filter(s => s.status === 'active' && isSessionStopped(s)).length;
@@ -579,6 +673,8 @@ ipcRoute('GET', '/api/host-overload/counts', (_req, res) => {
579
673
  continue;
580
674
  if (ds.adoptedFrom || ds.initConfig?.adoptMode)
581
675
  continue;
676
+ if (!isSuspendableBackendType(ds.initConfig?.backendType))
677
+ continue;
582
678
  if (ds.lastScreenStatus !== 'idle')
583
679
  continue;
584
680
  idle++;
@@ -588,9 +684,9 @@ ipcRoute('GET', '/api/host-overload/counts', (_req, res) => {
588
684
  /**
589
685
  * Bulk host-overload降压 sweep, driven by the overload-alert card buttons.
590
686
  * `mode`:
591
- * - `clean_stopped`: close stopped zombie sessions (dead CLI + no tmux) from
592
- * the SHARED session store machine-wide, so a single daemon's sweep is
593
- * enough (the alert-owning daemon calls this once, no fan-out needed).
687
+ * - `clean_stopped`: close stopped zombie sessions (dead CLI + no exact
688
+ * persistent backing) from THIS daemon's bot-scoped session store. The
689
+ * card handler fans this mode out to every online daemon.
594
690
  * - `suspend_idle`: suspend THIS daemon's own idle (non-busy, suspendable,
595
691
  * non-adopt) live workers. Live workers only exist in their owning daemon's
596
692
  * process, so the card handler fans this mode out to every online daemon.
@@ -611,10 +707,8 @@ ipcRoute('POST', '/api/host-overload/sweep', async (req, res) => {
611
707
  for (const s of stopped) {
612
708
  try {
613
709
  const r = await closeSession(s.sessionId);
614
- // Only count sessions this call actually closed. A shared-store session
615
- // already closed by another daemon's concurrent sweep returns
616
- // alreadyClosed=true — counting it would inflate `affected` by the
617
- // number of daemons that raced on the same zombie.
710
+ // Only count sessions this call actually closed. A concurrent action in
711
+ // this daemon may already have closed the same record.
618
712
  if (r.ok && !r.alreadyClosed)
619
713
  affected++;
620
714
  }
@@ -634,6 +728,8 @@ ipcRoute('POST', '/api/host-overload/sweep', async (req, res) => {
634
728
  continue; // no live worker
635
729
  if (ds.adoptedFrom || ds.initConfig?.adoptMode)
636
730
  continue; // never suspend adopt
731
+ if (!isSuspendableBackendType(ds.initConfig?.backendType))
732
+ continue;
637
733
  if (ds.lastScreenStatus !== 'idle')
638
734
  continue; // never cut an in-flight reply
639
735
  try {
@@ -692,14 +788,17 @@ ipcRoute('POST', '/api/sessions/:sessionId/slash', async (req, res, params) => {
692
788
  if (ds.adoptedFrom || ds.initConfig?.adoptMode) {
693
789
  return jsonRes(res, 409, { ok: false, error: 'adopt_inject_unsupported' });
694
790
  }
695
- if (!ds.worker || ds.worker.killed)
791
+ if ((!ds.worker || ds.worker.killed) && !isSessionTransferring(ds)) {
696
792
  return jsonRes(res, 409, { ok: false, error: 'no_live_worker' });
793
+ }
697
794
  const allow = getBotTuiSlashAllow(ds.larkAppId);
698
795
  const v = validateSlashInjection(body?.command ?? '', allow);
699
796
  if (!v.ok)
700
797
  return jsonRes(res, 403, { ok: false, error: v.error });
701
798
  try {
702
- ds.worker.send({ type: 'inject_command', command: v.command });
799
+ if (!sendWorkerSessionInput(ds, { type: 'inject_command', command: v.command })) {
800
+ return jsonRes(res, 409, { ok: false, error: 'no_live_worker' });
801
+ }
703
802
  }
704
803
  catch {
705
804
  // slash 注入无状态(不像 /cd 那样已 repin 记录),send 失败不需要杀进程
@@ -708,6 +807,76 @@ ipcRoute('POST', '/api/sessions/:sessionId/slash', async (req, res, params) => {
708
807
  }
709
808
  jsonRes(res, 200, { ok: true, sessionId: params.sessionId, queued: v.command });
710
809
  });
810
+ const proactiveChatRenameCooldown = new ChatRenameCooldown();
811
+ const chatRenameSerialQueue = new ChatRenameSerialQueue();
812
+ /** Session-scoped external mutation used by the botmux-chat-rename Skill. */
813
+ ipcRoute('POST', '/api/sessions/:sessionId/chat-rename', async (req, res, params) => {
814
+ const body = await readJsonBody(req)
815
+ .catch(() => ({}));
816
+ const ds = findActiveBySessionId(params.sessionId);
817
+ const auth = sessionCliIpcAuth(req, ds, params.sessionId, body);
818
+ if (!auth.ok)
819
+ return jsonRes(res, 403, { ok: false, error: auth.error });
820
+ if (!ds)
821
+ return jsonRes(res, 404, { ok: false, error: 'session_not_active' });
822
+ if (sessionTransportDisabled(ds))
823
+ return jsonRes(res, 200, { ok: false, error: 'no_feishu_transport' });
824
+ if (ds.chatType !== 'group')
825
+ return jsonRes(res, 400, { ok: false, error: 'not_group_chat' });
826
+ const normalized = normalizeLarkChatName(body.name);
827
+ if (!normalized.ok)
828
+ return jsonRes(res, 400, normalized);
829
+ const proactive = body.proactive === true;
830
+ const trigger = proactive ? 'ai_proactive' : 'user_explicit';
831
+ const cooldownKey = `${ds.larkAppId}:${ds.chatId}`;
832
+ await chatRenameSerialQueue.run(cooldownKey, async () => {
833
+ const result = await groupsStore.renameChat(ds.larkAppId, ds.chatId, normalized.name, {
834
+ beforeUpdate: proactive
835
+ ? () => {
836
+ const cooldown = proactiveChatRenameCooldown.check(cooldownKey);
837
+ return cooldown.ok
838
+ ? cooldown
839
+ : { ...cooldown, error: 'rate_limited' };
840
+ }
841
+ : undefined,
842
+ });
843
+ const botOpenId = getBotOpenId(ds.larkAppId) ?? '-';
844
+ if (!result.ok) {
845
+ const status = result.error === 'bot_not_in_chat' ? 403
846
+ : result.error === 'permission_denied' ? 403
847
+ : result.error === 'rate_limited' ? 429
848
+ : 502;
849
+ logger.warn(`[chat-rename:audit] result=failed session=${ds.session.sessionId} chat=${ds.chatId} `
850
+ + `app=${ds.larkAppId} botOpenId=${botOpenId} trigger=${trigger} `
851
+ + `old=${JSON.stringify(result.oldName ?? null)} new=${JSON.stringify(result.newName ?? normalized.name)} `
852
+ + `error=${result.error} larkCode=${result.larkCode ?? '-'} detail=${result.detail ?? '-'}`);
853
+ return jsonRes(res, status, result);
854
+ }
855
+ if (result.changed) {
856
+ if (proactive)
857
+ proactiveChatRenameCooldown.record(cooldownKey);
858
+ // FR-7: the Lark write already succeeded, so a local cache-refresh
859
+ // failure (ENOSPC/EACCES on the session store) must NOT reverse the
860
+ // outcome into an HTTP 500 — best-effort per session, warn and keep the
861
+ // rename a success. Catch per-session so one bad write can't skip the rest.
862
+ for (const active of getActiveSessionsRegistry()?.values() ?? []) {
863
+ if (active.chatId !== ds.chatId)
864
+ continue;
865
+ active.session.chatDisplayName = result.newName;
866
+ try {
867
+ sessionStore.updateSession(active.session);
868
+ }
869
+ catch (e) {
870
+ logger.warn(`[chat-rename:audit] cache_refresh_failed session=${active.session.sessionId} chat=${ds.chatId} app=${ds.larkAppId} detail=${e instanceof Error ? e.message : String(e)}`);
871
+ }
872
+ }
873
+ logger.info(`[chat-rename:audit] result=success session=${ds.session.sessionId} chat=${ds.chatId} `
874
+ + `app=${ds.larkAppId} botOpenId=${botOpenId} trigger=${trigger} `
875
+ + `old=${JSON.stringify(result.oldName)} new=${JSON.stringify(result.newName)} larkCode=0`);
876
+ }
877
+ return jsonRes(res, 200, { ...result, chatId: ds.chatId });
878
+ });
879
+ });
711
880
  /** 会话内切换工作目录(角色切换专用):硬校验角色库根 → 更新记录落盘(唯一事实源)
712
881
  * → 活 worker 走「带 --resume 的进程重启、respawn 在新 cwd」,无活 worker 杀残留
713
882
  * pane 让下条消息冷启动。
@@ -734,24 +903,46 @@ ipcRoute('POST', '/api/sessions/:sessionId/cd', async (req, res, params) => {
734
903
  return jsonRes(res, 403, { ok: false, error: auth.error });
735
904
  if (!ds)
736
905
  return jsonRes(res, 404, { ok: false, error: 'session_not_active' });
906
+ if (isSessionTransferring(ds)) {
907
+ return jsonRes(res, 409, { ok: false, error: 'session_transferring' });
908
+ }
737
909
  // Adopt/observed 会话是收编的用户自有 pane——注入或冷重启都会打断用户自己的
738
910
  // 终端会话。与 /suspend、/restart、/slash 同款排除。
739
911
  if (ds.adoptedFrom || ds.initConfig?.adoptMode) {
740
912
  return jsonRes(res, 409, { ok: false, error: 'adopt_cd_unsupported' });
741
913
  }
742
- const v = validateRoleLibraryPath(body?.dir ?? '');
914
+ // ownAppId 收窄到本 bot 自己的角色库子树:不收窄就能切进别的 bot 的角色目录,
915
+ // 下面 repinSessionWorkingDir 把 ds.workingDir 钉过去之后,那个 bot 的沙盒会话
916
+ // 就拿到了对方整棵角色库的 readWrite(打穿 fs-policy 的跨 bot 隔离)。
917
+ const v = validateRoleLibraryPath(body?.dir ?? '', undefined, ds.larkAppId);
743
918
  if (!v.ok) {
744
- return jsonRes(res, v.error === 'outside_role_library' ? 403 : 400, { ok: false, error: v.error });
919
+ const forbidden = v.error === 'outside_role_library' || v.error === 'outside_own_role_library';
920
+ // own_role_library_missing:本 bot 的 `<角色库根>/<appId>` 不是真目录(存量用
921
+ // 人类 slug 命名这一层,未按 deploy-runbook §8 迁移)。FAIL-CLOSED——不回落全局
922
+ // 根(回落是 fail-open,会让存量部署继续能跨 bot 切并经 workingDir 拿 rw)。回
923
+ // 409 + 迁移指引,让运营看得见查得到,而不是静默放行或静默拒绝。
924
+ if (v.error === 'own_role_library_missing') {
925
+ logger.warn(`[role] 角色库每-bot 目录名不是 appId(期望 ~/botmux-roles/${ds.larkAppId})——`
926
+ + 'role switch 已 fail-closed 拒绝,避免跨 bot 越权。按 docs/roles/deploy-runbook.md '
927
+ + '§8「迁移:每-bot 目录名改为 appId」重命名该目录即恢复。');
928
+ return jsonRes(res, 409, { ok: false, error: v.error });
929
+ }
930
+ return jsonRes(res, forbidden ? 403 : 400, { ok: false, error: v.error });
745
931
  }
746
932
  repinSessionWorkingDir(ds, v.resolvedPath);
933
+ // ds.initConfig 与 repin 同步,且**无条件**(不只在 live-worker 分支里):下次
934
+ // forkWorker 用 initConfig 重建 init 消息,只在有活 worker 时更新的话,no-worker /
935
+ // worker.killed 两条分支会留下「记录新、initConfig 旧」的分裂状态,冷启动把会话
936
+ // 带回旧 cwd(= 旧角色,连记忆桶都是旧的)。codex review 抓出的既有 bug,与本 PR
937
+ // 的收窄同一处理,顺手修掉。
938
+ if (ds.initConfig)
939
+ ds.initConfig.workingDir = v.resolvedPath;
747
940
  if (ds.worker && !ds.worker.killed) {
748
941
  // updateWorkingDir 随 restart 带给 worker:respawn 必须收敛到新目录,而不是
749
- // 陈旧的 lastInitConfig.workingDirdaemon 侧的 ds.initConfig 同步更新,保持
750
- // 与 worker 侧一致(下次 forkWorker 用它重建 init 消息)。
751
- if (ds.initConfig)
752
- ds.initConfig.workingDir = v.resolvedPath;
942
+ // 陈旧的 lastInitConfig.workingDirdaemon initConfig 已在上面同步)。
753
943
  try {
754
- ds.worker.send({ type: 'restart', updateWorkingDir: v.resolvedPath });
944
+ ds.workerReady = false;
945
+ ds.worker.send({ type: 'restart', updateWorkingDir: v.resolvedPath, env: latestPerBotEnvForRestart(ds) });
755
946
  }
756
947
  catch {
757
948
  // send() 抛异常:worker 进程实际上已经不可达(管道已断),但 above 的
@@ -778,6 +969,28 @@ ipcRoute('POST', '/api/sessions/:sessionId/cd', async (req, res, params) => {
778
969
  function findSessionRecord(sessionId) {
779
970
  return findActiveBySessionId(sessionId)?.session ?? sessionStore.getSession(sessionId);
780
971
  }
972
+ /** True when a session-bound IPC route must NOT touch Feishu: the owning bot is
973
+ * core-only (apiOnly) OR the session is an HTTP virtual chat. Central guard for
974
+ * every session-write route (chat-rename / write-link-card / resume-notice /
975
+ * locate / restart-notice …) — the daemon owns the authoritative bot config,
976
+ * so gating here catches the normal-bot-in-virtual-session case that
977
+ * getBotClient (which only throws for apiOnly) cannot. Accepts a live
978
+ * DaemonSession or a stored Session record. Never throws. */
979
+ function sessionTransportDisabled(s) {
980
+ const appId = s.larkAppId;
981
+ let apiOnly = false;
982
+ if (appId) {
983
+ try {
984
+ apiOnly = getBot(appId).config.apiOnly === true;
985
+ }
986
+ catch { /* unknown bot → not apiOnly */ }
987
+ }
988
+ return !larkTransportEnabled({ chatId: s.chatId ?? '', apiOnly });
989
+ }
990
+ /** Mutating IPC routes may only touch this daemon's own bot-partitioned store. */
991
+ function findOwnedSessionRecord(sessionId) {
992
+ return findActiveBySessionId(sessionId)?.session ?? sessionStore.getOwnedSession(sessionId);
993
+ }
781
994
  /** Four-state async lookup with durable fallback (design A).
782
995
  *
783
996
  * In-memory `asyncTriggerResults` lives only on the active DaemonSession and is
@@ -826,7 +1039,7 @@ function buildAsyncTriggerLookupResponse(sessionId, triggerId) {
826
1039
  sessionId,
827
1040
  liveActive: !!ds,
828
1041
  chatId: ds?.chatId ?? stored?.chatId,
829
- memResult: memResult ? { status: memResult.status, content: memResult.content, completedAt: memResult.completedAt } : undefined,
1042
+ memResult: memResult ? { status: memResult.status, content: memResult.content, completedAt: memResult.completedAt, usage: memResult.usage } : undefined,
830
1043
  memTriggerId: memResult ? memTriggerId : undefined,
831
1044
  persisted,
832
1045
  storedStatus: stored ? (stored.status === 'closed' ? 'closed' : 'open') : undefined,
@@ -848,7 +1061,7 @@ ipcRoute('POST', '/api/sessions/:sessionId/board', async (req, res, params) => {
848
1061
  const position = normalizeKanbanPosition(body.position);
849
1062
  if (!column && position === null)
850
1063
  return jsonRes(res, 400, { ok: false, error: 'bad_request' });
851
- const session = findSessionRecord(params.sessionId);
1064
+ const session = findOwnedSessionRecord(params.sessionId);
852
1065
  if (!session)
853
1066
  return jsonRes(res, 404, { ok: false, error: 'session_not_found' });
854
1067
  // 待办池(queued)会话被拖到「进行中」= 激活:把暂存内容当首轮发给 CLI 开跑。
@@ -968,6 +1181,12 @@ ipcRoute('GET', '/api/sessions/:sessionId/history', async (req, res, params) =>
968
1181
  const appId = session.larkAppId || cachedLarkAppId;
969
1182
  if (!appId)
970
1183
  return jsonRes(res, 422, { ok: false, error: 'no_lark_app' });
1184
+ // No-transport session (apiOnly bot or HTTP virtual chat) has no Feishu chat
1185
+ // history — listChatMessages/listThreadMessages would dial Feishu with a
1186
+ // synthetic chatId. Return empty history instead of making the network call.
1187
+ if (!larkTransportEnabled({ chatId: session.chatId, apiOnly: getBot(appId).config.apiOnly })) {
1188
+ return jsonRes(res, 200, { ok: true, messages: [], hint: 'no_feishu_transport' });
1189
+ }
971
1190
  const url = new URL(req.url ?? '/', 'http://localhost');
972
1191
  const limit = Math.min(Math.max(parseInt(url.searchParams.get('limit') ?? '80', 10) || 80, 1), 200);
973
1192
  try {
@@ -1130,12 +1349,7 @@ ipcRoute('GET', '/api/sessions/:sessionId/insight/turn/:turnIndex', (req, res, p
1130
1349
  ipcRoute('GET', '/api/insights/summary', async (req, res) => {
1131
1350
  const url = new URL(req.url ?? '/', 'http://localhost');
1132
1351
  const limit = Math.min(Math.max(parseInt(url.searchParams.get('limit') ?? '200', 10) || 200, 1), 500);
1133
- const active = listActiveSessions().map(composeRowFromActive);
1134
- const activeIds = new Set(active.map(r => r.sessionId));
1135
- const closed = sessionStore.listSessions()
1136
- .filter(s => s.status === 'closed' && !activeIds.has(s.sessionId))
1137
- .map(composeRowFromClosed);
1138
- const rows = [...active, ...closed];
1352
+ const rows = composeDashboardSessionRows();
1139
1353
  const overview = await buildSafeInsightOverview(rows.map(row => {
1140
1354
  const session = findSessionRecord(row.sessionId);
1141
1355
  return {
@@ -1176,20 +1390,30 @@ ipcRoute('POST', '/api/sessions/:sessionId/rename', async (req, res, params) =>
1176
1390
  catch {
1177
1391
  return jsonRes(res, 400, { ok: false, error: 'bad_json' });
1178
1392
  }
1393
+ const active = findActiveBySessionId(params.sessionId);
1394
+ const auth = sessionCliIpcAuth(req, active, params.sessionId, body);
1395
+ if (!auth.ok)
1396
+ return jsonRes(res, 403, { ok: false, error: auth.error });
1179
1397
  const title = normalizeSessionTitle(body.title);
1180
1398
  if (!title)
1181
1399
  return jsonRes(res, 400, { ok: false, error: 'bad_title' });
1182
- const active = findActiveBySessionId(params.sessionId);
1183
- const session = active?.session ?? sessionStore.getSession(params.sessionId);
1400
+ const session = active?.session ?? sessionStore.getOwnedSession(params.sessionId);
1184
1401
  if (!session)
1185
1402
  return jsonRes(res, 404, { ok: false, error: 'session_not_found' });
1186
- const updated = updateSessionTitle(session, title);
1403
+ const source = normalizeSessionTitleSource(body.source, 'dashboard');
1404
+ const updated = updateSessionTitle(session, title, source);
1187
1405
  if (!updated.ok)
1188
1406
  return jsonRes(res, 400, { ok: false, error: updated.error });
1189
1407
  const agentSync = active
1190
1408
  ? requestAgentSessionRename(active, updated.title)
1191
1409
  : { status: 'not_running' };
1192
- jsonRes(res, 200, { ...updated, agentSync: agentSync.status });
1410
+ jsonRes(res, 200, {
1411
+ ok: true,
1412
+ title: updated.title,
1413
+ titleUpdatedAt: updated.updatedAt,
1414
+ titleSource: updated.source,
1415
+ agentSync: agentSync.status,
1416
+ });
1193
1417
  });
1194
1418
  // 会话锁定:保护被锁定会话不被 dashboard「清理空闲」批量关闭。锁定是会话元数据,
1195
1419
  // 不影响用户显式点击关闭/批量关闭,避免把会话变成不可管理状态。
@@ -1203,7 +1427,7 @@ ipcRoute('POST', '/api/sessions/:sessionId/lock', async (req, res, params) => {
1203
1427
  }
1204
1428
  if (typeof body.locked !== 'boolean')
1205
1429
  return jsonRes(res, 400, { ok: false, error: 'bad_locked' });
1206
- const session = findSessionRecord(params.sessionId);
1430
+ const session = findOwnedSessionRecord(params.sessionId);
1207
1431
  if (!session)
1208
1432
  return jsonRes(res, 404, { ok: false, error: 'session_not_found' });
1209
1433
  if (body.locked)
@@ -1238,6 +1462,9 @@ ipcRoute('GET', '/api/sessions/:sessionId/write-link', (req, res, params) => {
1238
1462
  const ds = findActiveBySessionId(params.sessionId);
1239
1463
  if (!ds)
1240
1464
  return jsonRes(res, 404, { ok: false, error: 'session_not_active' });
1465
+ if (!sessionSupportsWebTerminal(ds)) {
1466
+ return jsonRes(res, 409, { ok: false, error: 'terminal_unsupported' });
1467
+ }
1241
1468
  // Riff backend: the sandbox URL is the writable link — no local worker needed.
1242
1469
  if (ds.riffAccessUrl) {
1243
1470
  jsonRes(res, 200, { ok: true, url: ds.riffAccessUrl });
@@ -1286,9 +1513,11 @@ ipcRoute('POST', '/api/sessions/:sessionId/write-link-card', async (req, res, pa
1286
1513
  const ds = findActiveBySessionId(params.sessionId);
1287
1514
  if (!ds)
1288
1515
  return jsonRes(res, 404, { ok: false, error: 'session_not_active' });
1516
+ if (sessionTransportDisabled(ds))
1517
+ return jsonRes(res, 200, { ok: false, error: 'no_feishu_transport' });
1289
1518
  const r = await deliverWriteLinkCardToOwners(ds);
1290
1519
  const status = r.ok ? 200
1291
- : r.error === 'terminal_unavailable' ? 409
1520
+ : r.error === 'terminal_unavailable' || r.error === 'terminal_unsupported' ? 409
1292
1521
  : r.error === 'no_owner' ? 422
1293
1522
  : 502;
1294
1523
  jsonRes(res, status, r);
@@ -1341,7 +1570,7 @@ ipcRoute('POST', '/api/sessions/:sessionId/resume', async (req, res, params) =>
1341
1570
  const cliId = ds.session.cliId;
1342
1571
  const cliName = getCliDisplayName(cliId ?? 'claude-code');
1343
1572
  const notice = JSON.stringify({ text: `🔄 会话已通过命令行恢复,发条消息继续与 ${cliName} 对话。` });
1344
- if (ds.larkAppId) {
1573
+ if (ds.larkAppId && !sessionTransportDisabled(ds)) {
1345
1574
  if (ds.scope === 'chat' && ds.chatId) {
1346
1575
  getChatMode(ds.larkAppId, ds.chatId, { forceRefresh: true })
1347
1576
  .then((mode) => mode === 'topic' && ds.session.rootMessageId
@@ -1520,6 +1749,11 @@ ipcRoute('POST', '/api/sessions/:sessionId/locate', async (_req, res, params) =>
1520
1749
  if (!ctx.ownerOpenId) {
1521
1750
  return jsonRes(res, 422, { ok: false, error: 'no_owner' });
1522
1751
  }
1752
+ // No-transport session (apiOnly bot or HTTP virtual chat) has no Feishu thread
1753
+ // to @-locate the owner in — the replyMessage below would dial Feishu.
1754
+ if (sessionTransportDisabled({ chatId: ds?.chatId ?? closed?.chatId, larkAppId: ctx.larkAppId })) {
1755
+ return jsonRes(res, 200, { ok: false, error: 'no_feishu_transport' });
1756
+ }
1523
1757
  try {
1524
1758
  const messageId = await replyMessage(ctx.larkAppId, ctx.rootMessageId, `<at user_id="${ctx.ownerOpenId}"></at>`, 'text', true);
1525
1759
  jsonRes(res, 200, { ok: true, messageId });
@@ -1913,12 +2147,13 @@ ipcRoute('GET', '/api/groups', async (_req, res) => {
1913
2147
  const enriched = chats.map(c => {
1914
2148
  const oncall = oncallStore.getOncallStatus(cachedLarkAppId, c.chatId);
1915
2149
  const hasRole = resolveRoleFile(cachedLarkAppId, c.chatId) !== null;
2150
+ const hasMessageListener = getMessageListenerConfig(cachedLarkAppId, c.chatId)?.enabled === true;
1916
2151
  // /introduce 记录的外部 botmux 机器人(按名字)——dashboard 团队看板用
1917
2152
  // 它识别「介绍过同团队机器人的协作群」。
1918
2153
  const observedBotNames = observedBotsStore
1919
2154
  .listObservedBots(config.session.dataDir, cachedLarkAppId, c.chatId)
1920
2155
  .map(b => b.name);
1921
- return { ...c, oncallChat: oncall ?? null, firstSeenAt: seenMap.get(c.chatId) ?? null, hasRole, observedBotNames };
2156
+ return { ...c, oncallChat: oncall ?? null, firstSeenAt: seenMap.get(c.chatId) ?? null, hasRole, hasMessageListener, observedBotNames };
1922
2157
  });
1923
2158
  jsonRes(res, 200, { chats: enriched });
1924
2159
  }
@@ -2155,6 +2390,290 @@ ipcRoute('DELETE', '/api/roles/:chatId', async (_req, res, p) => {
2155
2390
  deleteRoleInjectMode(cachedLarkAppId, p.chatId);
2156
2391
  jsonRes(res, 200, { ok: true, existed });
2157
2392
  });
2393
+ ipcRoute('GET', '/api/message-listeners/:chatId', async (_req, res, p) => {
2394
+ if (!cachedLarkAppId)
2395
+ return jsonRes(res, 503, { error: 'larkAppId_not_set' });
2396
+ if (!isValidRoleChatId(p.chatId))
2397
+ return jsonRes(res, 400, { ok: false, error: 'invalid_chat_id' });
2398
+ jsonRes(res, 200, {
2399
+ chatId: p.chatId,
2400
+ listener: getMessageListenerConfig(cachedLarkAppId, p.chatId),
2401
+ maxPromptBytes: MAX_MESSAGE_LISTENER_PROMPT_BYTES,
2402
+ });
2403
+ });
2404
+ ipcRoute('PUT', '/api/message-listeners/:chatId', async (req, res, p) => {
2405
+ if (!cachedLarkAppId)
2406
+ return jsonRes(res, 503, { error: 'larkAppId_not_set' });
2407
+ if (!isValidRoleChatId(p.chatId))
2408
+ return jsonRes(res, 400, { ok: false, error: 'invalid_chat_id' });
2409
+ let body;
2410
+ try {
2411
+ body = await readJsonBody(req);
2412
+ }
2413
+ catch {
2414
+ return jsonRes(res, 400, { ok: false, error: 'bad_json' });
2415
+ }
2416
+ const update = sanitizeMessageListenerUpdate(body);
2417
+ if (!update)
2418
+ return jsonRes(res, 400, { ok: false, error: 'invalid_listener' });
2419
+ const validation = validateMessageListenerUpdate(update);
2420
+ if (!validation.ok)
2421
+ return jsonRes(res, 400, { ok: false, error: validation.reason });
2422
+ if (update.prompt && Buffer.byteLength(update.prompt, 'utf-8') > MAX_MESSAGE_LISTENER_PROMPT_BYTES) {
2423
+ return jsonRes(res, 400, { ok: false, error: 'prompt_too_large' });
2424
+ }
2425
+ const result = await updateMessageListenerConfig(cachedLarkAppId, p.chatId, update);
2426
+ if (!result.ok)
2427
+ return jsonRes(res, ['prompt_required', 'sender_required'].includes(result.reason) ? 400 : 500, { ok: false, error: result.reason });
2428
+ jsonRes(res, 200, { ok: true, listener: result.listener });
2429
+ });
2430
+ function dashboardHistoryMessageSender(message) {
2431
+ const sender = message?.sender ?? {};
2432
+ const senderId = sender.id ?? sender.open_id ?? sender.user_id ?? sender.app_id
2433
+ ?? message?.sender_id?.open_id ?? message?.sender_id?.user_id ?? message?.sender_id?.app_id;
2434
+ const senderName = sender.sender_name ?? sender.name ?? sender.user_name ?? message?.sender_name;
2435
+ const senderIdType = sender.id_type ?? sender.sender_id_type;
2436
+ const senderTypeRaw = sender.sender_type ?? message?.sender_type ?? (senderIdType === 'app_id' ? 'app' : undefined);
2437
+ return {
2438
+ senderOpenId: typeof senderId === 'string' ? senderId : undefined,
2439
+ senderName: typeof senderName === 'string' && senderName.trim() ? senderName.trim() : undefined,
2440
+ senderTypeRaw: typeof senderTypeRaw === 'string' ? senderTypeRaw : undefined,
2441
+ senderIdType: typeof senderIdType === 'string' ? senderIdType : undefined,
2442
+ };
2443
+ }
2444
+ function dashboardMessageCreateTimeMs(message) {
2445
+ const value = Number(message?.create_time ?? message?.createTime);
2446
+ return Number.isFinite(value) ? value : undefined;
2447
+ }
2448
+ async function readMessageListenerPreviewRequest(req) {
2449
+ let body;
2450
+ try {
2451
+ body = await readJsonBody(req);
2452
+ }
2453
+ catch {
2454
+ return { ok: false, status: 400, error: 'bad_json' };
2455
+ }
2456
+ const raw = body && typeof body === 'object' && !Array.isArray(body) ? body : {};
2457
+ const listener = sanitizeMessageListenerUpdate(raw.listener ?? raw);
2458
+ if (!listener)
2459
+ return { ok: false, status: 400, error: 'invalid_listener' };
2460
+ const validation = validateMessageListenerUpdate(listener);
2461
+ if (!validation.ok)
2462
+ return { ok: false, status: 400, error: validation.reason };
2463
+ if (listener.prompt && Buffer.byteLength(listener.prompt, 'utf-8') > MAX_MESSAGE_LISTENER_PROMPT_BYTES) {
2464
+ return { ok: false, status: 400, error: 'prompt_too_large' };
2465
+ }
2466
+ return { ok: true, listener, limit: normalizeMessageListenerPreviewLimit(raw.limit) };
2467
+ }
2468
+ async function collectMessageListenerPreviewMatches(larkAppId, chatId, listener, limit) {
2469
+ const bot = getBot(larkAppId);
2470
+ const previewListener = {
2471
+ enabled: true,
2472
+ ...(listener.name ? { name: listener.name } : {}),
2473
+ ...(listener.replyCardTitle ? { replyCardTitle: listener.replyCardTitle } : {}),
2474
+ ...(listener.workingDir ? { workingDir: listener.workingDir } : {}),
2475
+ prompt: listener.prompt,
2476
+ ...(listener.senderPolicy && Object.keys(listener.senderPolicy).length > 0 ? { senderPolicy: listener.senderPolicy } : {}),
2477
+ ...(listener.messagePolicy ? { messagePolicy: { ...listener.messagePolicy, scope: 'top_level' } } : { messagePolicy: { scope: 'top_level' } }),
2478
+ replyPolicy: { mode: 'thread', sessionMode: 'per_message' },
2479
+ };
2480
+ const previewBot = {
2481
+ ...bot,
2482
+ config: {
2483
+ ...bot.config,
2484
+ messageListeners: {
2485
+ ...(bot.config.messageListeners ?? {}),
2486
+ [chatId]: previewListener,
2487
+ },
2488
+ },
2489
+ };
2490
+ const cutoff = Date.now() - MESSAGE_LISTENER_PREVIEW_WINDOW_MS;
2491
+ const messages = await listChatMessagesUntil(larkAppId, chatId, {
2492
+ pageSize: 50,
2493
+ stopAfter: (message, seenCount) => {
2494
+ const createdAt = dashboardMessageCreateTimeMs(message);
2495
+ return seenCount >= Math.max(100, limit * 5) ||
2496
+ (Number.isFinite(createdAt) && createdAt < cutoff);
2497
+ },
2498
+ });
2499
+ const candidateBotAppIds = collectListenerBotAppIds(messages, dashboardHistoryMessageSender);
2500
+ const appIdToOpenId = await buildListenerBotAppIdToOpenId(larkAppId, chatId, candidateBotAppIds);
2501
+ return previewMessageListenerMatches({
2502
+ bot: previewBot,
2503
+ chatId,
2504
+ messages,
2505
+ limit,
2506
+ senderForMessage: dashboardHistoryMessageSender,
2507
+ appIdToOpenId,
2508
+ // Mirror realtime/poll routing: a message that explicitly @mentions this bot
2509
+ // hands off to normal @-routing, NOT the listener — so preview/run-preview
2510
+ // must apply the same gate (else preview over-counts and run-preview would
2511
+ // spawn a session for a message live routing never sends to the listener).
2512
+ explicitlyMentionedThisBot: (message) => messageMentionsBot(message, larkAppId, bot.botOpenId),
2513
+ });
2514
+ }
2515
+ function publicMessageListenerMatch(match) {
2516
+ return {
2517
+ messageId: match.messageId,
2518
+ createTime: match.createTime,
2519
+ messageText: match.messageText,
2520
+ messageTitle: match.messageTitle,
2521
+ msgType: match.msgType,
2522
+ senderOpenId: match.senderOpenId,
2523
+ senderName: match.senderName,
2524
+ senderType: match.senderType,
2525
+ };
2526
+ }
2527
+ ipcRoute('POST', '/api/message-listeners/:chatId/preview', async (req, res, p) => {
2528
+ if (!cachedLarkAppId)
2529
+ return jsonRes(res, 503, { ok: false, error: 'larkAppId_not_set' });
2530
+ if (!isValidRoleChatId(p.chatId))
2531
+ return jsonRes(res, 400, { ok: false, error: 'invalid_chat_id' });
2532
+ const parsed = await readMessageListenerPreviewRequest(req);
2533
+ if (!parsed.ok)
2534
+ return jsonRes(res, parsed.status, { ok: false, error: parsed.error });
2535
+ try {
2536
+ const matches = await collectMessageListenerPreviewMatches(cachedLarkAppId, p.chatId, parsed.listener, parsed.limit);
2537
+ jsonRes(res, 200, {
2538
+ ok: true,
2539
+ requestedLimit: parsed.limit,
2540
+ matches: matches.map(publicMessageListenerMatch),
2541
+ });
2542
+ }
2543
+ catch (err) {
2544
+ jsonRes(res, 502, { ok: false, error: err instanceof Error ? err.message : String(err) });
2545
+ }
2546
+ });
2547
+ ipcRoute('POST', '/api/message-listeners/:chatId/run-preview', async (req, res, p) => {
2548
+ if (!cachedLarkAppId)
2549
+ return jsonRes(res, 503, { ok: false, error: 'larkAppId_not_set' });
2550
+ if (!isValidRoleChatId(p.chatId))
2551
+ return jsonRes(res, 400, { ok: false, error: 'invalid_chat_id' });
2552
+ const activeSessions = getActiveSessionsRegistry();
2553
+ if (!activeSessions)
2554
+ return jsonRes(res, 503, { ok: false, error: 'active session registry unavailable' });
2555
+ const parsed = await readMessageListenerPreviewRequest(req);
2556
+ if (!parsed.ok)
2557
+ return jsonRes(res, parsed.status, { ok: false, error: parsed.error });
2558
+ try {
2559
+ const matches = await collectMessageListenerPreviewMatches(cachedLarkAppId, p.chatId, parsed.listener, parsed.limit);
2560
+ const run = createMessageListenerRunPreview(cachedLarkAppId, p.chatId, matches.map(match => match.messageId));
2561
+ const results = [];
2562
+ for (const match of matches) {
2563
+ const triggerId = createMessageListenerRunPreviewTurnId();
2564
+ try {
2565
+ const result = await triggerSessionTurn({
2566
+ source: {
2567
+ type: 'ui',
2568
+ connectorId: 'message-listener-preview',
2569
+ requestId: `listener-preview:${match.messageId}`,
2570
+ receivedAt: new Date().toISOString(),
2571
+ },
2572
+ target: {
2573
+ kind: 'turn',
2574
+ botId: cachedLarkAppId,
2575
+ chatId: p.chatId,
2576
+ rootMessageId: match.messageId,
2577
+ },
2578
+ envelope: {
2579
+ format: 'message_listener',
2580
+ sourceName: match.name || 'Message Listener Preview',
2581
+ trusted: false,
2582
+ payload: publicMessageListenerMatch(match),
2583
+ rawText: match.messageText,
2584
+ },
2585
+ instruction: renderMessageListenerInstruction(match),
2586
+ presentation: { topicMessage: null },
2587
+ }, { larkAppId: cachedLarkAppId, activeSessions }, { stableTurnId: triggerId });
2588
+ const tracked = result.ok
2589
+ ? markMessageListenerRunPreviewTriggered(run.runId, match.messageId, {
2590
+ action: result.action,
2591
+ sessionId: result.target?.sessionId,
2592
+ triggerId: result.triggerId ?? triggerId,
2593
+ })
2594
+ : markMessageListenerRunPreviewFailed(run.runId, {
2595
+ messageId: match.messageId,
2596
+ sessionId: result.target?.sessionId,
2597
+ error: result.error,
2598
+ });
2599
+ results.push(tracked ?? {
2600
+ runId: run.runId,
2601
+ messageId: match.messageId,
2602
+ ok: result.ok,
2603
+ state: result.ok ? 'triggered' : 'failed',
2604
+ action: result.action,
2605
+ sessionId: result.target?.sessionId,
2606
+ triggerId: result.triggerId ?? triggerId,
2607
+ error: result.error,
2608
+ });
2609
+ }
2610
+ catch (err) {
2611
+ const error = err instanceof Error ? err.message : String(err);
2612
+ const tracked = markMessageListenerRunPreviewFailed(run.runId, {
2613
+ messageId: match.messageId,
2614
+ error,
2615
+ });
2616
+ results.push(tracked ?? {
2617
+ runId: run.runId,
2618
+ messageId: match.messageId,
2619
+ ok: false,
2620
+ state: 'failed',
2621
+ error,
2622
+ });
2623
+ }
2624
+ }
2625
+ jsonRes(res, 200, {
2626
+ ok: results.every(result => result.ok),
2627
+ runId: run.runId,
2628
+ requestedLimit: parsed.limit,
2629
+ matches: matches.map(publicMessageListenerMatch),
2630
+ results,
2631
+ });
2632
+ }
2633
+ catch (err) {
2634
+ jsonRes(res, 502, { ok: false, error: err instanceof Error ? err.message : String(err) });
2635
+ }
2636
+ });
2637
+ ipcRoute('GET', '/api/message-listeners/:chatId/run-preview/:runId', async (_req, res, p) => {
2638
+ if (!cachedLarkAppId)
2639
+ return jsonRes(res, 503, { ok: false, error: 'larkAppId_not_set' });
2640
+ if (!isValidRoleChatId(p.chatId))
2641
+ return jsonRes(res, 400, { ok: false, error: 'invalid_chat_id' });
2642
+ const run = getMessageListenerRunPreview(p.runId);
2643
+ if (!run || run.larkAppId !== cachedLarkAppId || run.chatId !== p.chatId) {
2644
+ return jsonRes(res, 404, { ok: false, error: 'not_found' });
2645
+ }
2646
+ jsonRes(res, 200, {
2647
+ ok: true,
2648
+ runId: run.runId,
2649
+ createdAt: run.createdAt,
2650
+ updatedAt: run.updatedAt,
2651
+ results: run.results,
2652
+ });
2653
+ });
2654
+ ipcRoute('DELETE', '/api/message-listeners/:chatId', async (_req, res, p) => {
2655
+ if (!cachedLarkAppId)
2656
+ return jsonRes(res, 503, { error: 'larkAppId_not_set' });
2657
+ if (!isValidRoleChatId(p.chatId))
2658
+ return jsonRes(res, 400, { ok: false, error: 'invalid_chat_id' });
2659
+ const result = await updateMessageListenerConfig(cachedLarkAppId, p.chatId, { enabled: false, prompt: '' });
2660
+ if (!result.ok)
2661
+ return jsonRes(res, 500, { ok: false, error: result.reason });
2662
+ jsonRes(res, 200, { ok: true });
2663
+ });
2664
+ ipcRoute('GET', '/api/groups/:chatId/members-display', async (_req, res, p) => {
2665
+ if (!cachedLarkAppId)
2666
+ return jsonRes(res, 503, { error: 'larkAppId_not_set' });
2667
+ if (!isValidRoleChatId(p.chatId))
2668
+ return jsonRes(res, 400, { ok: false, error: 'invalid_chat_id' });
2669
+ try {
2670
+ const members = await listChatMemberDisplays(cachedLarkAppId, p.chatId);
2671
+ jsonRes(res, 200, { members });
2672
+ }
2673
+ catch (err) {
2674
+ jsonRes(res, 502, { ok: false, error: err instanceof Error ? err.message : String(err) });
2675
+ }
2676
+ });
2158
2677
  // ─── Role profile management (dashboard) ──────────────────────────────────
2159
2678
  // Profiles are authoring/storage helpers only; applying one writes this bot's
2160
2679
  // entry into the selected chat role and does not alter runtime role layering.
@@ -2416,6 +2935,11 @@ ipcRoute('GET', '/api/bot-default-oncall', async (_req, res) => {
2416
2935
  // disables the toggle wherever the worker would fail-close on it.
2417
2936
  readIsolationSupported: readIsolationEnforceable(cachedLarkAppId),
2418
2937
  backendType: backendTypeStore.getBotBackendType(cachedLarkAppId) ?? null,
2938
+ usageDisplay: cardPrefs.usageDisplay,
2939
+ // Whether this bot's CLI can produce native usage at all. When false the
2940
+ // dashboard hides the usage-display control (offering it would be a knob
2941
+ // that is always empty — the CLI has no resolvable transcript).
2942
+ usageSupported: cliSupportsNativeUsage(cliId),
2419
2943
  disableStreamingCard: cardPrefs.disableStreamingCard,
2420
2944
  silentTurnReactions: cardPrefs.silentTurnReactions,
2421
2945
  codexAppCleanInput: cardPrefs.codexAppCleanInput,
@@ -2466,6 +2990,8 @@ ipcRoute('PUT', '/api/bot-card-prefs', async (req, res) => {
2466
2990
  return jsonRes(res, 400, { ok: false, error: 'bad_json' });
2467
2991
  }
2468
2992
  const patch = {};
2993
+ if (body.usageDisplay === 'streaming' || body.usageDisplay === 'footer' || body.usageDisplay === 'off')
2994
+ patch.usageDisplay = body.usageDisplay;
2469
2995
  if (typeof body.disableStreamingCard === 'boolean')
2470
2996
  patch.disableStreamingCard = body.disableStreamingCard;
2471
2997
  if (typeof body.botToBotSameDir === 'boolean')
@@ -2522,6 +3048,9 @@ ipcRoute('PUT', '/api/bot-substitute-mode', async (req, res) => {
2522
3048
  const chats = Array.isArray(rec.chats)
2523
3049
  ? [...new Set(rec.chats.map(String).map(s => s.trim()).filter(Boolean))]
2524
3050
  : [];
3051
+ const excludedChats = Array.isArray(rec.excludedChats)
3052
+ ? [...new Set(rec.excludedChats.map(String).map(s => s.trim()).filter(Boolean))]
3053
+ : [];
2525
3054
  const r = await substituteModeStore.updateBotSubstituteMode(cachedLarkAppId, {
2526
3055
  enabled: rec.enabled === true,
2527
3056
  targets,
@@ -2529,6 +3058,7 @@ ipcRoute('PUT', '/api/bot-substitute-mode', async (req, res) => {
2529
3058
  replyMode: rec.replyMode === 'quote' ? 'quote' : 'thread',
2530
3059
  disableControlCard: rec.disableControlCard === true,
2531
3060
  ...(chats.length ? { chats } : {}),
3061
+ ...(excludedChats.length ? { excludedChats } : {}),
2532
3062
  // 话题群开关:显式 false 才关(旧客户端不带字段 → normalize 缺省开)。
2533
3063
  topicGroups: rec.topicGroups,
2534
3064
  topicActiveSessionTrigger: rec.topicActiveSessionTrigger,
@@ -2592,7 +3122,7 @@ ipcRoute('PUT', '/api/bot-summary-trigger', async (req, res) => {
2592
3122
  // Per-bot 授权偏好。Body 任意子集:
2593
3123
  // • restrictGrantCommands: boolean — 限制被授权人只能纯对话
2594
3124
  // • autoGrantRequestCards: boolean — 未授权 @ 被挡住时是否发 grant 申请卡
2595
- // • messageQuotaDefaultLimit: number|null — 默认消息额度(null = 关闭,正整数 = 启用)
3125
+ // • messageQuotaDefaultLimit: number|null — 卡片默认额度覆盖(null = 产品默认 3 条)
2596
3126
  ipcRoute('PUT', '/api/bot-grant-prefs', async (req, res) => {
2597
3127
  if (!cachedLarkAppId)
2598
3128
  return jsonRes(res, 503, { error: 'larkAppId_not_set' });
@@ -3287,7 +3817,7 @@ ipcRoute('PUT', '/api/bot-read-isolation', async (req, res) => {
3287
3817
  const suspendedSessions = await suspendActiveSessionsForBot(cachedLarkAppId);
3288
3818
  jsonRes(res, 200, { ok: true, readIsolation: r.readIsolation, suspendedSessions });
3289
3819
  });
3290
- // Per-bot session backend override (pty | tmux | herdr | zellij), or clear it
3820
+ // Per-bot session backend override (pty | tmux | herdr | zellij | zmx), or clear it
3291
3821
  // ('' / 'auto' / null → follow the daemon default). next-session 生效:running
3292
3822
  // sessions keep their spawn-time backend (Session.backendType stamp), only new
3293
3823
  // spawns read the new value — so switching here can't strand live sessions.
@@ -3608,6 +4138,15 @@ ipcRoute('GET', '/api/events', (_req, res) => {
3608
4138
  activeIds.add(ds.session.sessionId);
3609
4139
  res.write(`event: session.spawned\ndata: ${JSON.stringify({ session: composeRowFromActive(ds) })}\n\n`);
3610
4140
  }
4141
+ // Persisted active rows may be intentionally absent from the runtime Map
4142
+ // after an inconclusive exact-backend teardown. Replay them as dormant
4143
+ // upserts so SSE reconnects retain the same truthful state as GET
4144
+ // /api/sessions and never synthesize a closed row.
4145
+ for (const s of sessionStore.listSessions()) {
4146
+ if (s.status !== 'active' || activeIds.has(s.sessionId))
4147
+ continue;
4148
+ res.write(`event: session.spawned\ndata: ${JSON.stringify({ session: composeRowFromPersistedActive(s) })}\n\n`);
4149
+ }
3611
4150
  // Also replay sessions CLOSED during this run as `session.spawned` carrying a
3612
4151
  // closed row. The active-only replay above can't cover a restore-time zombie:
3613
4152
  // restoreActiveSessions registers it, announces it, then immediately probes it
@@ -3645,7 +4184,16 @@ export function startIpcServer(opts) {
3645
4184
  try {
3646
4185
  const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
3647
4186
  const method = req.method ?? 'GET';
3648
- const publicRoute = routeHasPublicAccess(method, url.pathname);
4187
+ const coreOnlyPublic = opts.coreOnlyPublicRoutes === true && routeIsCoreOnlyPublic(method, url.pathname);
4188
+ const publicRoute = routeHasPublicAccess(method, url.pathname) || coreOnlyPublic;
4189
+ // Readiness barrier (codex P1): the core-only public control routes
4190
+ // (trigger / trigger-result / insight) must NOT enter their handlers until
4191
+ // restore completes — a trigger during 'starting' races durable restore.
4192
+ // Gate them at the server level so it doesn't depend on the caller probing
4193
+ // /healthz first. /healthz itself reports 503 via its own handler.
4194
+ if (coreOnlyPublic && coreOnlyNotReady()) {
4195
+ return jsonRes(res, 503, { ok: false, status: 'starting', error: 'core-only service is still restoring; retry after /healthz returns 200' });
4196
+ }
3649
4197
  const capabilityRoute = routeHasNarrowUntrustedAuth(method, url.pathname);
3650
4198
  if (opts.authRequired && !publicRoute) {
3651
4199
  const secret = ipcAuthSecret();
@@ -3687,6 +4235,7 @@ export function startIpcServer(opts) {
3687
4235
  server,
3688
4236
  port: opts.port,
3689
4237
  host: opts.host,
4238
+ maxProbe: opts.maxProbe,
3690
4239
  log: (m) => logger.warn(`[dashboard-ipc] ${m}`),
3691
4240
  }).then((port) => {
3692
4241
  boundPort = port;