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
@@ -12,17 +12,18 @@ import * as sessionStore from '../services/session-store.js';
12
12
  import * as messageQueue from '../services/message-queue.js';
13
13
  import { downloadMessageResource, listChatBotMembers, UserTokenMissingError } from '../im/lark/client.js';
14
14
  import { logger } from '../utils/logger.js';
15
- import { forkWorker, sendWorkerInput, forkAdoptWorker, adoptSandboxBlocked, killStalePids, getCurrentCliVersion, restoreUsageLimitRuntimeState, setActiveSessionSafe, isRelayableRealSession, closeSession, getActiveSessionsRegistry, suspendWorker } from './worker-pool.js';
15
+ import { forkWorker, sendWorkerInput, forkAdoptWorker, adoptSandboxBlocked, killStalePids, sweepDeadPidMarkers, getCurrentCliVersion, restoreUsageLimitRuntimeState, setActiveSessionIfActive, setActiveSessionSafe, isDisposableCommandScratch, isRelayableRealSession, closeSession, getActiveSessionsRegistry, suspendWorker, isSessionTransferring, deferUntilSessionTransferSettled } from './worker-pool.js';
16
16
  import { createCliAdapterSync } from '../adapters/cli/registry.js';
17
17
  import { buildBotmuxShellHints } from '../adapters/cli/shared-hints.js';
18
18
  import { resolveSkillInjectionModeForApp, builtinSkillEntries, buildBuiltinSkillCatalogBlock, builtinSkillHelpPointer, } from '../skills/injection-mode.js';
19
- import { getSessionPersistentBackendType, persistentBackendTargetForSession, probePersistentBackendTarget, probePersistentBackendServer, killPersistentBackendTarget, } from './persistent-backend.js';
19
+ import { getSessionPersistentBackendType, persistentBackendTargetForSession, probePersistentBackendServer, probePersistentBackendTarget, probePersistentSessions, killPersistentBackendTarget, } from './persistent-backend.js';
20
20
  import { adoptTargetLabel, validateAdoptTargetState } from './session-discovery.js';
21
21
  import { getBot, getAllBots, getOwnerOpenId, findOncallChat, effectiveDefaultWorkingDir } from '../bot-registry.js';
22
22
  import { dashboardEventBus } from './dashboard-events.js';
23
- import { composeRowFromActive } from './dashboard-rows.js';
23
+ import { composeRowFromActive, composeRowFromPersistedActive } from './dashboard-rows.js';
24
24
  import { composeSpawnCodexAppContext, composeSpawnUserContent, deriveSessionTitleFromContent, } from './session-create.js';
25
25
  import { validateZellijAdoptTarget } from './zellij-adopt-discovery.js';
26
+ import { backendSupportsWebTerminal } from '../adapters/backend/capabilities.js';
26
27
  import { addCodexAppContext } from '../utils/codex-app-context.js';
27
28
  import { activeSessionKey, sessionKey, sessionAnchorId, storedSessionAnchorId } from './types.js';
28
29
  import { announceSessionRow, markSessionActivity, announcePendingRepoSession } from './session-activity.js';
@@ -40,6 +41,7 @@ import { getAttachmentsDir } from './attachment-path.js';
40
41
  import { resolveRegularGroupMode } from '../services/chat-reply-mode-store.js';
41
42
  import { beginReplyTargetTurn } from './reply-target.js';
42
43
  import { readDeferredTopicBinding, removeDeferredTopicBinding } from './deferred-topic-binding.js';
44
+ import { escapeXmlTagLikeTokens } from '../utils/xml.js';
43
45
  export { getAttachmentsDir } from './attachment-path.js';
44
46
  function sessionCreatedAtMs(session) {
45
47
  return session.createdAt ? (Date.parse(session.createdAt) || Date.now()) : Date.now();
@@ -47,6 +49,31 @@ function sessionCreatedAtMs(session) {
47
49
  function sessionLastMessageAtMs(session) {
48
50
  return session.lastMessageAt ? (Date.parse(session.lastMessageAt) || sessionCreatedAtMs(session)) : sessionCreatedAtMs(session);
49
51
  }
52
+ function quarantineUnregisteredRestoreSession(session, reason) {
53
+ session.restoreQuarantinedAt ??= new Date().toISOString();
54
+ try {
55
+ sessionStore.updateSession(session);
56
+ }
57
+ catch (err) {
58
+ logger.error(`[${session.sessionId.substring(0, 8)}] Could not persist restore quarantine (${reason}): `
59
+ + `${err instanceof Error ? err.message : String(err)}`);
60
+ }
61
+ // This row was intentionally never announced as a runtime DaemonSession.
62
+ // Upsert a dormant persisted projection so an already-open dashboard does not
63
+ // lose it merely because its exact backing teardown was inconclusive.
64
+ try {
65
+ dashboardEventBus.publish({
66
+ type: 'session.spawned',
67
+ body: { session: composeRowFromPersistedActive(session) },
68
+ });
69
+ }
70
+ catch (err) {
71
+ // Visibility projection is best-effort; it must never turn one isolated
72
+ // teardown failure into a daemon restore failure for every later session.
73
+ logger.error(`[${session.sessionId.substring(0, 8)}] Could not publish restore quarantine row: `
74
+ + `${err instanceof Error ? err.message : String(err)}`);
75
+ }
76
+ }
50
77
  function sameUsageLimit(a, b) {
51
78
  if (!a && !b)
52
79
  return true;
@@ -82,11 +109,32 @@ function sessionBotCliMismatch(ds) {
82
109
  }
83
110
  return null;
84
111
  }
112
+ const cliMismatchResweepArmed = new WeakSet();
113
+ function armCliMismatchResweep(ds) {
114
+ if (cliMismatchResweepArmed.has(ds))
115
+ return;
116
+ cliMismatchResweepArmed.add(ds);
117
+ const rerun = () => {
118
+ cliMismatchResweepArmed.delete(ds);
119
+ void closeCliMismatchedSessionsForBot(ds.larkAppId).catch((err) => {
120
+ logger.error(`[${ds.session.sessionId.substring(0, 8)}] Deferred CLI mismatch resweep failed: `
121
+ + `${err instanceof Error ? err.message : String(err)}`);
122
+ });
123
+ };
124
+ if (!deferUntilSessionTransferSettled(ds, rerun)) {
125
+ // The gate settled between the caller's guard and registration.
126
+ queueMicrotask(rerun);
127
+ }
128
+ }
85
129
  async function closeActiveSessionIfCliMismatch(ds) {
86
130
  const mismatch = sessionBotCliMismatch(ds);
87
131
  if (!mismatch)
88
- return false;
132
+ return 'not_mismatched';
89
133
  const tag = ds.session.sessionId.substring(0, 8);
134
+ if (isSessionTransferring(ds)) {
135
+ logger.warn(`[${tag}] CLI mismatch close deferred until routing transfer settles`);
136
+ return 'transfer_deferred';
137
+ }
90
138
  const backendType = getSessionPersistentBackendType(ds);
91
139
  // 仅在没有活 worker 时预杀 backing pane:restore 守卫处 ds 尚未进 registry,
92
140
  // closeSession→killWorker 摸不到 pane,必须在这里亲手杀;而活 worker(运行时
@@ -95,13 +143,25 @@ async function closeActiveSessionIfCliMismatch(ds) {
95
143
  if (backendType && (!ds.worker || ds.worker.killed)) {
96
144
  const target = persistentBackendTargetForSession(ds);
97
145
  logger.warn(`[${tag}] CLI mismatch (session=${mismatch.sessionCli}, bot=${mismatch.botCli}), closing active session and killing ${backendType} ${target.sessionName}${target.backendType === 'herdr' && target.agentName ? `/${target.agentName}` : ''}`);
98
- killPersistentBackendTarget(target);
146
+ try {
147
+ killPersistentBackendTarget(target, ds.session.sessionId);
148
+ }
149
+ catch (err) {
150
+ // ZMX destruction is deliberately identity-verified and can fail on an
151
+ // inconclusive probe or a generation change. Never close the persisted
152
+ // row in that state: doing so would orphan a possibly-live CLI with no
153
+ // retryable ownership record. The caller skips this row and continues
154
+ // restoring/sweeping unrelated sessions.
155
+ logger.error(`[${tag}] CLI mismatch backing teardown failed; keeping active row: `
156
+ + `${err instanceof Error ? err.message : String(err)}`);
157
+ return 'teardown_failed';
158
+ }
99
159
  }
100
160
  else {
101
161
  logger.warn(`[${tag}] CLI mismatch (session=${mismatch.sessionCli}, bot=${mismatch.botCli}), closing active session`);
102
162
  }
103
163
  await closeSession(ds.session.sessionId);
104
- return true;
164
+ return 'closed';
105
165
  }
106
166
  /**
107
167
  * Runtime counterpart of the restore-time CLI-mismatch guard(#346 只堵了重启
@@ -126,8 +186,21 @@ export async function closeCliMismatchedSessionsForBot(larkAppId) {
126
186
  continue;
127
187
  if (ds.adoptedFrom || ds.session.adoptedFrom || ds.session.title?.startsWith('Adopt:'))
128
188
  continue;
129
- if (await closeActiveSessionIfCliMismatch(ds))
130
- closed++;
189
+ if (isSessionTransferring(ds)) {
190
+ armCliMismatchResweep(ds);
191
+ continue;
192
+ }
193
+ try {
194
+ const result = await closeActiveSessionIfCliMismatch(ds);
195
+ if (result === 'closed')
196
+ closed++;
197
+ else if (result === 'transfer_deferred')
198
+ armCliMismatchResweep(ds);
199
+ }
200
+ catch (err) {
201
+ logger.error(`[${ds.session.sessionId.substring(0, 8)}] CLI mismatch close failed; continuing sweep: `
202
+ + `${err instanceof Error ? err.message : String(err)}`);
203
+ }
131
204
  }
132
205
  return closed;
133
206
  }
@@ -153,6 +226,10 @@ export async function suspendActiveSessionsForBot(larkAppId) {
153
226
  continue;
154
227
  if (ds.adoptedFrom || ds.session.adoptedFrom || ds.session.title?.startsWith('Adopt:'))
155
228
  continue;
229
+ if (isSessionTransferring(ds)) {
230
+ logger.warn(`[${ds.session.sessionId.substring(0, 8)}] Read-isolation cold restart deferred during routing transfer`);
231
+ continue;
232
+ }
156
233
  // Prefer suspend (keeps the session; --resume continues context on the next
157
234
  // message). But suspendWorker no-ops for a NON-suspendable backend (explicit
158
235
  // PTY) — leaving the old unisolated process running would silently defeat the
@@ -502,35 +579,13 @@ function renderWhiteboardBlock(opts) {
502
579
  return [
503
580
  `<whiteboard id="${id}">`,
504
581
  '本地项目上下文;读取:`botmux whiteboard read --id ' + id + ' --json`(拿到 content 与 updatedAt)。',
505
- '更新状态:`botmux whiteboard update --id ' + id + ' --expected-updated-at <上次 read 的 updatedAt> <内容>`。',
582
+ escapeXmlTagLikeTokens('更新状态:`botmux whiteboard update --id ' + id + ' --expected-updated-at <上次 read 的 updatedAt> <内容>`。'),
506
583
  '更新前先用 `read --json` 拿到当前内容与 updatedAt,融合新信息后整体重写为一份完整的当前状态(默认中文;代码标识/命令/错误信息可保留原文),并用 `--expected-updated-at` 回传 read 到的版本号做并发冲突检测。',
507
584
  '若更新报 `whiteboard_cas_mismatch`,说明期间有其它 agent 改过白板——重新 `read --json` 拿最新内容与 updatedAt,再次融合重写。',
508
585
  '不要直接读写本地文件;不要写密钥/隐私;用户可见结论仍必须 `botmux send`。',
509
586
  '</whiteboard>',
510
587
  ].join('\n');
511
588
  }
512
- function buildHermesBotmuxHints(locale) {
513
- if (locale === 'en') {
514
- return [
515
- 'You are running in a Feishu/Lark chat through botmux. For ordinary text replies, write the user-facing answer as your final assistant message; botmux automatically forwards that final output to Feishu/Lark.',
516
- 'Do not call `botmux send` for normal text answers. Use `botmux send` only for special delivery needs: files/images/videos, voice, cross-chat/top-level sends, or explicit mention routing to another person/bot.',
517
- '`botmux send` / `botmux history` / `botmux quoted` / `botmux bots` are shell commands installed in $PATH; run them via Bash/terminal tools when needed.',
518
- 'If you already used `botmux send` for special delivery in this turn, do not put a second copy of the answer, messageId, or send-success receipt in the final assistant message.',
519
- ];
520
- }
521
- return [
522
- '你运行在飞书(Lark)聊天中。普通文字回复请直接写在 assistant final 里,botmux 会自动把 final_output 转发到飞书。',
523
- '普通文本答案不要调用 `botmux send`。只有需要图片/文件/视频/语音、跨群或顶层发送、显式 @ 某人/某 bot 等特殊投递能力时,才使用 `botmux send`。',
524
- '`botmux send` / `botmux history` / `botmux quoted` / `botmux bots` 是已安装在 $PATH 的 shell 命令;需要时通过 Bash/terminal 工具执行。',
525
- '如果本轮已经为了特殊投递调用过 `botmux send`,final 里不要再写第二份正文、messageId 或“发送成功/已处理”回执。',
526
- ];
527
- }
528
- function hermesFollowupReminder(locale) {
529
- if (locale === 'en') {
530
- return 'For ordinary text replies, do not call `botmux send`; put the user-facing answer in final and botmux will forward it to Feishu/Lark. Use `botmux send` only for special delivery such as files/images/videos, voice, cross-chat/top-level sends, or explicit mention routing. If already used, do not add a second answer or send-success receipt in final.';
531
- }
532
- return '普通文字回复不要调用 `botmux send`;直接把给用户看的答案写在 final,botmux 会自动转发到飞书。只有图片/文件/视频/语音、跨群/顶层发送、特殊 @ 路由等特殊投递才用 `botmux send`;如果本轮已经用过,不要在 final 里再写第二份答案或发送成功回执。';
533
- }
534
589
  /**
535
590
  * Peer count at/below which the `<available_bots>` block inlines the full
536
591
  * roster (name + open_id). Above it the block collapses to a one-line pointer
@@ -591,7 +646,7 @@ export function buildNewTopicPrompt(userMessage, sessionId, cliId, cliPathOverri
591
646
  // (Claude Code builds its own via --append-system-prompt). Source hints
592
647
  // freshly from i18n so they respect the resolved locale instead of the
593
648
  // static `adapter.systemHints` array that was baked at module load.
594
- const hints = adapter.injectsSessionContext ? [] : (cliId === 'hermes' ? buildHermesBotmuxHints(locale) : buildBotmuxShellHints(locale));
649
+ const hints = adapter.injectsSessionContext ? [] : buildBotmuxShellHints(locale);
595
650
  const routingBlock = hints.length > 0
596
651
  ? `<botmux_routing>\n${hints.join('\n')}\n</botmux_routing>`
597
652
  : '';
@@ -622,7 +677,7 @@ export function buildNewTopicPrompt(userMessage, sessionId, cliId, cliPathOverri
622
677
  '<identity>',
623
678
  ` <name>${xmlEscape(botIdentity.name ?? unknown)}</name>`,
624
679
  ` <open_id>${xmlEscape(botIdentity.openId ?? unknown)}</open_id>`,
625
- ` <routing_rules>${t('ai.identity.short_routing', undefined, locale)}</routing_rules>`,
680
+ ` <routing_rules>${escapeXmlTagLikeTokens(t('ai.identity.short_routing', undefined, locale))}</routing_rules>`,
626
681
  '</identity>',
627
682
  ].join('\n');
628
683
  }
@@ -744,9 +799,13 @@ export function buildFollowUpContent(content, sessionId, opts) {
744
799
  if (roleBlock)
745
800
  parts.push(roleBlock);
746
801
  if (opts?.cliId !== 'mira') {
747
- const reminder = opts?.cliId === 'hermes'
748
- ? hermesFollowupReminder(opts?.locale)
749
- : t('ai.followup.reminder', undefined, opts?.locale);
802
+ // All non-Mira CLIs including Hermes, which no longer gets reverse
803
+ // send-first guidance (#653) and now shares this standard path — get the
804
+ // anti-resend variant only when the experimental dashboard toggle is on
805
+ // (config.noVisibleOutputHint, default OFF); otherwise the reminder is
806
+ // byte-for-byte the pre-feature baseline. Live-read so a Settings flip
807
+ // applies to the next follow-up turn without a daemon restart.
808
+ const reminder = t(config.noVisibleOutputHint ? 'ai.followup.reminder_no_resend' : 'ai.followup.reminder', undefined, opts?.locale);
750
809
  parts.push(`<botmux_reminder>${reminder}</botmux_reminder>`);
751
810
  }
752
811
  if (whiteboardBlock)
@@ -1013,7 +1072,7 @@ export function rememberLastCliInput(ds, userPrompt, cliInput, opts) {
1013
1072
  // ─── Session restore ─────────────────────────────────────────────────────────
1014
1073
  /**
1015
1074
  * Whether daemon restore should eagerly re-fork a worker to re-attach a
1016
- * surviving backing pane. True for every persistent backend (tmux/herdr/zellij);
1075
+ * surviving backing pane. True for every persistent backend (tmux/herdr/zellij/zmx);
1017
1076
  * the pty backend has nothing to re-attach to, so it stays lazy.
1018
1077
  *
1019
1078
  * Eager re-attach is what makes a session actually come back after a restart —
@@ -1054,15 +1113,53 @@ export async function staggeredRecoveryFork(sessions, fork, batchSize = RECOVERY
1054
1113
  }
1055
1114
  export async function restoreActiveSessions(activeSessions) {
1056
1115
  const sessions = sessionStore.listSessions();
1057
- const active = sessions.filter(s => s.status === 'active');
1116
+ const restorePriority = (session) => {
1117
+ if (session.adoptedFrom || session.cliId || session.lastCliInput || session.backendType)
1118
+ return 2;
1119
+ if (session.queued)
1120
+ return 1;
1121
+ return 0; // disposable daemon-command scratch
1122
+ };
1123
+ // Deterministic winner policy for corrupt/legacy same-anchor duplicates:
1124
+ // real CLI/adopt rows first, queued intent second, command scratches last.
1125
+ // Registration itself is CAS, so a fresh runtime occupant always wins over
1126
+ // every startup candidate regardless of this disk ordering.
1127
+ const active = sessions
1128
+ .filter(s => s.status === 'active')
1129
+ .sort((a, b) => restorePriority(b) - restorePriority(a));
1130
+ // Sweep dead CLI-pid markers regardless of whether we have sessions to restore:
1131
+ // the landmine files (recycled-PID misroute source) accumulate across every
1132
+ // daemon run, and a run with zero active sessions is still a fresh start that
1133
+ // should clean up after prior crashes/SIGKILLs.
1134
+ sweepDeadPidMarkers();
1058
1135
  if (active.length === 0) {
1059
1136
  logger.info('No active sessions to restore');
1060
1137
  return;
1061
1138
  }
1062
1139
  // Kill any stale CLI processes from previous daemon run
1063
- killStalePids(active);
1140
+ // Dispatcher/IPC are already live while restore runs. Pass the runtime map
1141
+ // so the stale-PID sweep cannot kill a fresh worker that won the startup
1142
+ // race for the same logical session. The full snapshot still reaches backend
1143
+ // cleanup, preserving its backing-session name as active.
1144
+ killStalePids(active, activeSessions);
1064
1145
  logger.info(`Registering ${active.length} active session(s) (no CLI spawn until new messages arrive)...`);
1146
+ const runtimeWinnerFor = (sessionId, candidate) => [...activeSessions.values()].find(ds => ds !== candidate && ds.session.sessionId === sessionId);
1147
+ // Persistent recovery below must only inspect rows registered by THIS
1148
+ // restore pass. The dispatcher/IPC are already live and may add a fresh
1149
+ // runtime session while one of the CAS registrations awaits; sweeping the
1150
+ // whole live Map would then mistake that new session for a startup snapshot
1151
+ // row and could close it while its backing pane is still being created.
1152
+ const restoredByThisInvocation = [];
1153
+ const stillOwnsRestoreRegistration = (ds) => ds.session.status === 'active'
1154
+ && activeSessions.get(activeSessionKey(ds)) === ds;
1065
1155
  for (const session of active) {
1156
+ // Dispatcher/IPC may create and register this exact persisted row while
1157
+ // startup restore is running. The runtime object is authoritative; never
1158
+ // rebuild or later close it as a collision loser.
1159
+ if (runtimeWinnerFor(session.sessionId)) {
1160
+ logger.debug(`[${session.sessionId.substring(0, 8)}] Already registered by live runtime during restore; skipping snapshot row`);
1161
+ continue;
1162
+ }
1066
1163
  // Restored sessions persisted before the scope field was added default to
1067
1164
  // 'thread' — that matches the legacy thread-only behaviour.
1068
1165
  const scope = session.scope === 'chat' ? 'chat' : 'thread';
@@ -1086,7 +1183,7 @@ export async function restoreActiveSessions(activeSessions) {
1086
1183
  }
1087
1184
  catch { /* unknown bot → only the frozen decision matters */ }
1088
1185
  if (adoptSandboxBlocked(adoptBotCfg, session)) {
1089
- logger.warn(`[${session.sessionId.substring(0, 8)}] sandbox session persisted as adopt — converting to cold-start (a sandbox can't wrap a live CLI)`);
1186
+ logger.warn(`[${session.sessionId.substring(0, 8)}] isolated/no-transport session persisted as adopt — converting to cold-start (a sandbox / apiOnly bot can't wrap a live external CLI)`);
1090
1187
  session.adoptedFrom = undefined;
1091
1188
  if (session.title?.startsWith('Adopt:')) {
1092
1189
  const project = session.title.slice('Adopt:'.length).trim();
@@ -1108,7 +1205,8 @@ export async function restoreActiveSessions(activeSessions) {
1108
1205
  continue;
1109
1206
  }
1110
1207
  if (validation === 'unknown') {
1111
- logger.warn(`Keeping adopt session ${session.sessionId} closed until next resume (target validation failed: ${adoptTargetLabel(adopted)})`);
1208
+ logger.warn(`Keeping adopt session ${session.sessionId} active but quarantined until the target can be verified (target validation failed: ${adoptTargetLabel(adopted)})`);
1209
+ quarantineUnregisteredRestoreSession(session, 'adopt_target_validation_unknown');
1112
1210
  continue;
1113
1211
  }
1114
1212
  // Original CLI still alive — re-register and fork adopt worker
@@ -1154,9 +1252,21 @@ export async function restoreActiveSessions(activeSessions) {
1154
1252
  // Same-key collision guard: if a prior iteration already set an entry
1155
1253
  // at this key (legitimately possible if disk holds two active sessions
1156
1254
  // resolving to the same chat-scope key — e.g. a leaked scratch +
1157
- // relayed real session from a prior buggy run), close the loser
1158
- // rather than silently overwriting it.
1159
- await setActiveSessionSafe(activeSessions, activeSessionKey(ds), ds);
1255
+ // relayed real session from a prior buggy run), reject and close the
1256
+ // incoming loser rather than silently overwriting the runtime winner.
1257
+ const adoptKey = activeSessionKey(ds);
1258
+ const adoptRegistered = await setActiveSessionSafe(activeSessions, adoptKey, ds);
1259
+ const adoptCurrent = activeSessions.get(adoptKey);
1260
+ if (runtimeWinnerFor(session.sessionId, ds)) {
1261
+ logger.debug(`[${session.sessionId.substring(0, 8)}] Live runtime won adopt restore registration`);
1262
+ continue;
1263
+ }
1264
+ if (!adoptRegistered || session.status !== 'active' || adoptCurrent !== ds) {
1265
+ logger.warn(`[${session.sessionId.substring(0, 8)}] Adopt restore was cancelled while resolving a routing collision`);
1266
+ await closeSession(session.sessionId);
1267
+ continue;
1268
+ }
1269
+ restoredByThisInvocation.push(ds);
1160
1270
  announceSessionRow(ds);
1161
1271
  forkAdoptWorker(ds, { restoredFromMetadata: true });
1162
1272
  logger.info(`[${session.sessionId.substring(0, 8)}] Restored adopt session (target: ${adoptTargetLabel(adopted)}, scope: ${scope})`);
@@ -1198,7 +1308,19 @@ export async function restoreActiveSessions(activeSessions) {
1198
1308
  };
1199
1309
  const anchor = sessionAnchorId(ds);
1200
1310
  messageQueue.ensureQueue(anchor);
1201
- await setActiveSessionSafe(activeSessions, activeSessionKey(ds), ds);
1311
+ const queuedKey = activeSessionKey(ds);
1312
+ const queuedRegistered = await setActiveSessionSafe(activeSessions, queuedKey, ds);
1313
+ const queuedCurrent = activeSessions.get(queuedKey);
1314
+ if (runtimeWinnerFor(session.sessionId, ds)) {
1315
+ logger.debug(`[${session.sessionId.substring(0, 8)}] Live runtime won queued restore registration`);
1316
+ continue;
1317
+ }
1318
+ if (!queuedRegistered || session.status !== 'active' || queuedCurrent !== ds) {
1319
+ logger.warn(`[${session.sessionId.substring(0, 8)}] Queued restore was cancelled while resolving a routing collision`);
1320
+ await closeSession(session.sessionId);
1321
+ continue;
1322
+ }
1323
+ restoredByThisInvocation.push(ds);
1202
1324
  // 重启后把待办池卡片重新广播给 dashboard,否则会从看板消失(#277 同款修复,
1203
1325
  // 我这条 queued 分支提前 continue 绕过了下面的 announceSessionRow,要自己补)。
1204
1326
  announceSessionRow(ds);
@@ -1250,7 +1372,19 @@ export async function restoreActiveSessions(activeSessions) {
1250
1372
  // instead of resurrecting an invisible worker.
1251
1373
  const backendType = getSessionPersistentBackendType(ds);
1252
1374
  if (backendType) {
1253
- killPersistentBackendTarget(persistentBackendTargetForSession(ds));
1375
+ try {
1376
+ killPersistentBackendTarget(persistentBackendTargetForSession(ds), session.sessionId);
1377
+ }
1378
+ catch (err) {
1379
+ // Keep the row active when destruction cannot be proved. Closing it
1380
+ // would strand a possibly-live hidden run with no ownership record;
1381
+ // skipping registration also prevents this restore pass from
1382
+ // accidentally reattaching it. Continue with the remaining rows.
1383
+ logger.error(`[${session.sessionId.substring(0, 8)}] Could not tear down unmaterialized deferred run; keeping active row: `
1384
+ + `${err instanceof Error ? err.message : String(err)}`);
1385
+ quarantineUnregisteredRestoreSession(session, 'deferred_run_teardown_unverified');
1386
+ continue;
1387
+ }
1254
1388
  }
1255
1389
  sessionStore.closeSession(session.sessionId);
1256
1390
  removeDeferredTopicBinding(config.session.dataDir, session.sessionId);
@@ -1268,14 +1402,49 @@ export async function restoreActiveSessions(activeSessions) {
1268
1402
  ds.replyThreadAliases = aliases;
1269
1403
  sessionStore.updateSession(session);
1270
1404
  }
1271
- if (await closeActiveSessionIfCliMismatch(ds))
1405
+ try {
1406
+ const mismatchClose = await closeActiveSessionIfCliMismatch(ds);
1407
+ if (mismatchClose !== 'not_mismatched') {
1408
+ if (mismatchClose === 'teardown_failed') {
1409
+ quarantineUnregisteredRestoreSession(session, 'cli_mismatch_teardown_unverified');
1410
+ }
1411
+ continue;
1412
+ }
1413
+ }
1414
+ catch (err) {
1415
+ logger.error(`[${session.sessionId.substring(0, 8)}] CLI mismatch close failed during restore; keeping active row: `
1416
+ + `${err instanceof Error ? err.message : String(err)}`);
1417
+ quarantineUnregisteredRestoreSession(session, 'cli_mismatch_close_failed');
1272
1418
  continue;
1419
+ }
1273
1420
  const anchor = sessionAnchorId(ds);
1274
1421
  messageQueue.ensureQueue(anchor);
1275
1422
  if (ds.usageLimit)
1276
1423
  restoreUsageLimitRuntimeState(ds);
1277
1424
  // Same-key collision guard — see adopt-branch comment above.
1278
- await setActiveSessionSafe(activeSessions, activeSessionKey(ds), ds);
1425
+ const restoreKey = activeSessionKey(ds);
1426
+ const restored = await setActiveSessionSafe(activeSessions, restoreKey, ds);
1427
+ const restoreCurrent = activeSessions.get(restoreKey);
1428
+ if (runtimeWinnerFor(session.sessionId, ds)) {
1429
+ logger.debug(`[${session.sessionId.substring(0, 8)}] Live runtime won restore registration`);
1430
+ continue;
1431
+ }
1432
+ if (!restored || session.status !== 'active' || restoreCurrent !== ds) {
1433
+ logger.warn(`[${session.sessionId.substring(0, 8)}] Restore was cancelled while resolving a routing collision`);
1434
+ try {
1435
+ await closeSession(session.sessionId);
1436
+ }
1437
+ catch (err) {
1438
+ // A rejected ZMX ownership/generation probe means teardown was not
1439
+ // proven. Keep the persisted row active for a later retry and continue
1440
+ // restoring unrelated sessions instead of crash-looping daemon boot.
1441
+ logger.error(`[${session.sessionId.substring(0, 8)}] Could not close restore collision loser; keeping active row: `
1442
+ + `${err instanceof Error ? err.message : String(err)}`);
1443
+ quarantineUnregisteredRestoreSession(session, 'restore_collision_close_failed');
1444
+ }
1445
+ continue;
1446
+ }
1447
+ restoredByThisInvocation.push(ds);
1279
1448
  announceSessionRow(ds);
1280
1449
  if (session.initialUserTurnPending) {
1281
1450
  // `hasHistory: true` above means "there may be a CLI process/transcript to
@@ -1292,6 +1461,51 @@ export async function restoreActiveSessions(activeSessions) {
1292
1461
  // actual re-fork is deferred into `toReattach` and staggered below so a box
1293
1462
  // with dozens of surviving sessions doesn't spike on restart.
1294
1463
  const toReattach = [];
1464
+ const restoreCandidates = [];
1465
+ const namesByBackend = new Map();
1466
+ for (const ds of restoredByThisInvocation) {
1467
+ // A later restore CAS awaited after this row was registered. During that
1468
+ // yield the user may have closed/resumed/replaced it; never carry the stale
1469
+ // object into a session-id based close that could hit its fresh successor.
1470
+ // A real message may also have synchronously forked this exact ds while the
1471
+ // registration Promise yielded. Its backing pane can still be starting, so
1472
+ // a missing probe is not zombie evidence and restore must leave it alone.
1473
+ if (!stillOwnsRestoreRegistration(ds) || ds.worker)
1474
+ continue;
1475
+ // External /adopt sessions use their discovered source target rather than
1476
+ // Botmux's deterministic managed backing name. They were already restored
1477
+ // through the adopt path above and must not enter the managed probe batch.
1478
+ if (ds.adoptedFrom)
1479
+ continue;
1480
+ // Queued(待办池)会话从没起过 CLI,没有任何后端会话——别去探它,否则 tmux 后端
1481
+ // 会把「找不到 backing」误判成僵尸而关掉它。
1482
+ if (ds.session.queued)
1483
+ continue;
1484
+ const backendType = getSessionPersistentBackendType(ds);
1485
+ if (!backendType || !shouldAutoForkOnRestore(backendType))
1486
+ continue;
1487
+ // Honour the worker-selected target (Herdr may own an agent inside a shared
1488
+ // host session) rather than assuming the deterministic whole-session name.
1489
+ const backendTarget = persistentBackendTargetForSession(ds);
1490
+ const backendName = backendTarget.backendType === 'herdr' && backendTarget.agentName
1491
+ ? `${backendTarget.sessionName}/${backendTarget.agentName}`
1492
+ : backendTarget.sessionName;
1493
+ restoreCandidates.push({ ds, backendType, backendTarget, backendName });
1494
+ // Only session-name-addressable targets can be answered from a batch
1495
+ // snapshot; agent-scoped Herdr rows fall back to their per-target probe.
1496
+ if (backendTarget.backendType === 'herdr' && backendTarget.agentName)
1497
+ continue;
1498
+ const names = namesByBackend.get(backendType) ?? new Set();
1499
+ names.add(backendTarget.sessionName);
1500
+ namesByBackend.set(backendType, names);
1501
+ }
1502
+ // ZMX/Zellij can classify every requested name from one control-plane list.
1503
+ // This is both a consistent restore snapshot and avoids an O(N²) ZMX restart
1504
+ // when each per-row probe would otherwise scan every per-session daemon.
1505
+ const probeSnapshots = new Map();
1506
+ for (const [backendType, names] of namesByBackend) {
1507
+ probeSnapshots.set(backendType, probePersistentSessions(backendType, names));
1508
+ }
1295
1509
  // Server-liveness is sampled ONCE per backend type (cached): a single
1296
1510
  // `tmux list-sessions` answers for all of that backend's sessions, and a
1297
1511
  // consistent snapshot avoids a mid-loop race where an early lazy fork could
@@ -1305,27 +1519,18 @@ export async function restoreActiveSessions(activeSessions) {
1305
1519
  }
1306
1520
  return s;
1307
1521
  };
1308
- for (const [, ds] of activeSessions) {
1309
- // Queued(待办池)会话从没起过 CLI,没有任何后端会话——别去探它,否则 tmux 后端
1310
- // 会把「找不到 backing」误判成僵尸而关掉它。
1311
- if (ds.session.queued)
1522
+ for (const { ds, backendType, backendTarget, backendName } of restoreCandidates) {
1523
+ // An earlier candidate's mismatch close can await document cleanup, so
1524
+ // revalidate exact ownership and worker state for every row before any
1525
+ // destructive action. A message can wake a later candidate during that
1526
+ // await, while its persistent backing is still being created.
1527
+ if (!stillOwnsRestoreRegistration(ds) || ds.worker)
1312
1528
  continue;
1313
- // External /adopt sessions were already validated and re-forked above
1314
- // against their real tmux/zellij target. They never own Botmux's
1315
- // deterministic bmx-<sessionId> backing session, so probing that name here
1316
- // would immediately zombie-close the session we just restored.
1317
- if (ds.adoptedFrom)
1318
- continue;
1319
- const backendType = getSessionPersistentBackendType(ds);
1320
- if (!backendType)
1321
- continue;
1322
- if (!shouldAutoForkOnRestore(backendType))
1323
- continue;
1324
- const backendTarget = persistentBackendTargetForSession(ds);
1325
- const backendName = backendTarget.backendType === 'herdr' && backendTarget.agentName
1326
- ? `${backendTarget.sessionName}/${backendTarget.agentName}`
1327
- : backendTarget.sessionName;
1328
- const probe = probePersistentBackendTarget(backendTarget);
1529
+ // Agent-scoped Herdr targets are not addressable by session name, so they
1530
+ // never joined the batch and keep the per-target probe.
1531
+ const probe = backendTarget.backendType === 'herdr' && backendTarget.agentName
1532
+ ? probePersistentBackendTarget(backendTarget)
1533
+ : probeSnapshots.get(backendType)?.get(backendTarget.sessionName) ?? 'unknown';
1329
1534
  if (probe === 'missing') {
1330
1535
  const tag = ds.session.sessionId.substring(0, 8);
1331
1536
  // Intentionally cold-resume-suspended (idle-worker sweeper killed the
@@ -1337,6 +1542,14 @@ export async function restoreActiveSessions(activeSessions) {
1337
1542
  logger.info(`[${tag}] ${backendType} session was cap-suspended — keeping active for lazy cold-resume`);
1338
1543
  continue;
1339
1544
  }
1545
+ // ZMX is not a shared multiplexer server: every session owns an
1546
+ // independent daemon. Another live ZMX session says nothing about this
1547
+ // one, so "missing" cannot safely drive a destructive zombie close.
1548
+ // Preserve the transcript-backed row and cold-resume on the next message.
1549
+ if (backendType === 'zmx') {
1550
+ logger.warn(`[${tag}] zmx backing session "${backendName}" is missing — keeping active for lazy resume`);
1551
+ continue;
1552
+ }
1340
1553
  // 'missing' is ambiguous: it means EITHER this one pane is gone while the
1341
1554
  // server runs (a true solo zombie) OR the whole multiplexer server is down
1342
1555
  // (e.g. machine reboot) and every pane vanished at once. Only the former is
@@ -1371,8 +1584,16 @@ export async function restoreActiveSessions(activeSessions) {
1371
1584
  // check on the reattach path too — persistent-backend reattach ignores the
1372
1585
  // bin/args handed to backend.spawn(), so anything that slips through here
1373
1586
  // would silently resurrect the old frozen CLI.
1374
- if (await closeActiveSessionIfCliMismatch(ds))
1587
+ try {
1588
+ const mismatchClose = await closeActiveSessionIfCliMismatch(ds);
1589
+ if (mismatchClose !== 'not_mismatched')
1590
+ continue;
1591
+ }
1592
+ catch (err) {
1593
+ logger.error(`[${ds.session.sessionId.substring(0, 8)}] CLI mismatch close failed during reattach; keeping active row: `
1594
+ + `${err instanceof Error ? err.message : String(err)}`);
1375
1595
  continue;
1596
+ }
1376
1597
  const tag = ds.session.sessionId.substring(0, 8);
1377
1598
  logger.info(`[${tag}] ${backendType} session alive, queued for re-attach`);
1378
1599
  toReattach.push(ds);
@@ -1404,6 +1625,10 @@ export async function restoreActiveSessions(activeSessions) {
1404
1625
  * only the first forks; the rest just await the same `ds.workerPort`.
1405
1626
  */
1406
1627
  export async function ensureTerminalWorkerPort(ds) {
1628
+ const frozenBackendType = ds.initConfig?.backendType ?? ds.session.backendType;
1629
+ if (frozenBackendType !== undefined && !backendSupportsWebTerminal(frozenBackendType)) {
1630
+ return undefined;
1631
+ }
1407
1632
  if (ds.workerPort)
1408
1633
  return ds.workerPort;
1409
1634
  if (ds.session.status !== 'active')
@@ -1455,6 +1680,7 @@ export async function ensureTerminalWorkerPort(ds) {
1455
1680
  * manual resume could resurrect a stale member epoch
1456
1681
  * - 'deferred_unmaterialized' — a silent fresh-topic run finished without
1457
1682
  * publishing, so it has no conversation to resume
1683
+ * - 'resume_cancelled' — a concurrent close won while resume was committing
1458
1684
  */
1459
1685
  export async function resumeSession(sessionId, activeSessions) {
1460
1686
  const session = sessionStore.getSession(sessionId);
@@ -1506,10 +1732,14 @@ export async function resumeSession(sessionId, activeSessions) {
1506
1732
  // throwaway command container, so clobbering it would lose real intent.
1507
1733
  const existing = activeSessions.get(key);
1508
1734
  if (existing) {
1509
- if (isRelayableRealSession(existing) || existing.pendingRepo) {
1735
+ if (!isDisposableCommandScratch(existing)) {
1510
1736
  return { ok: false, error: 'anchor_occupied', activeSessionId: existing.session.sessionId };
1511
1737
  }
1512
1738
  await closeSession(existing.session.sessionId);
1739
+ const replacement = activeSessions.get(key);
1740
+ if (replacement) {
1741
+ return { ok: false, error: 'anchor_occupied', activeSessionId: replacement.session.sessionId };
1742
+ }
1513
1743
  }
1514
1744
  // Belt-and-suspenders: also scan persisted sessions for any *other* active
1515
1745
  // session pinned to the same (larkAppId, scope, anchor). The in-memory Map
@@ -1541,13 +1771,21 @@ export async function resumeSession(sessionId, activeSessions) {
1541
1771
  && (s.larkAppId ?? '') === larkAppId
1542
1772
  && (s.scope === 'chat' ? 'chat' : 'thread') === scope
1543
1773
  && storedSessionAnchorId(s) === anchor);
1544
- const realConflict = conflicts.find(s => !!s.cliId || !!s.lastCliInput);
1774
+ const realConflict = conflicts.find(s => !!s.cliId || !!s.lastCliInput || !!s.backendType || !!s.adoptedFrom || s.queued === true);
1545
1775
  if (realConflict) {
1546
1776
  return { ok: false, error: 'anchor_occupied', activeSessionId: realConflict.sessionId };
1547
1777
  }
1548
1778
  for (const scratch of conflicts) {
1549
1779
  await closeSession(scratch.sessionId);
1550
1780
  }
1781
+ const replacementAfterDiskCleanup = activeSessions.get(key);
1782
+ if (replacementAfterDiskCleanup) {
1783
+ return {
1784
+ ok: false,
1785
+ error: 'anchor_occupied',
1786
+ activeSessionId: replacementAfterDiskCleanup.session.sessionId,
1787
+ };
1788
+ }
1551
1789
  // Reactivate in store — clear closedAt so dashboard rows don't keep showing
1552
1790
  // the stale close timestamp on the now-active session.
1553
1791
  session.status = 'active';
@@ -1584,9 +1822,18 @@ export async function resumeSession(sessionId, activeSessions) {
1584
1822
  };
1585
1823
  messageQueue.ensureQueue(anchor);
1586
1824
  // setActiveSessionSafe over a bare Map.set: the scratch-eviction above
1587
- // should already have freed `key`, but if any occupant remains it closes
1588
- // it rather than silently orphaning it (consistent with restore/transfer).
1589
- await setActiveSessionSafe(activeSessions, key, ds);
1825
+ // should already have freed `key`, but its compare-and-set gate lets any
1826
+ // concurrent runtime occupant win rather than silently orphaning it.
1827
+ const registered = await setActiveSessionSafe(activeSessions, key, ds);
1828
+ if (!registered || session.status !== 'active' || activeSessions.get(key) !== ds) {
1829
+ const occupant = activeSessions.get(key);
1830
+ if (session.status === 'active')
1831
+ await closeSession(sessionId);
1832
+ if (occupant && occupant !== ds) {
1833
+ return { ok: false, error: 'anchor_occupied', activeSessionId: occupant.session.sessionId };
1834
+ }
1835
+ return { ok: false, error: 'resume_cancelled' };
1836
+ }
1590
1837
  logger.info(`Resumed session ${sessionId.substring(0, 8)} (scope: ${scope}, anchor: ${anchor.substring(0, 12)})`);
1591
1838
  return { ok: true, ds };
1592
1839
  }
@@ -1811,41 +2058,73 @@ export async function executeScheduledTask(task, activeSessions, refreshCliVersi
1811
2058
  const firePrompt = silent
1812
2059
  ? `${buildSilentScheduleHint(task.name, localeForBot(larkAppId))}\n\n${task.prompt}`
1813
2060
  : task.prompt;
1814
- // Inject into a live session if one already exists at this anchor.
1815
- const existing = activeSessions.get(sessionKey(anchor, larkAppId));
1816
- if (isContinuation && existing?.worker && !existing.worker.killed) {
2061
+ // Continue through the session already registered at this anchor. A
2062
+ // deliberately suspended session keeps its active registration with
2063
+ // worker=null; scheduled turns must cold-resume that same session instead of
2064
+ // creating a competing row that the registration CAS will reject.
2065
+ const activeKey = sessionKey(anchor, larkAppId);
2066
+ const existing = activeSessions.get(activeKey);
2067
+ const blockedDeferredSetup = existing && (existing.session.queued
2068
+ || existing.pendingRepo
2069
+ || existing.pendingRepoCommitInFlight
2070
+ || existing.worktreeCreating);
2071
+ if (isContinuation && blockedDeferredSetup) {
2072
+ // Dashboard backlog and repo/worktree setup rows deliberately own the route
2073
+ // while keeping their worker null. Cold-resuming one with the scheduled
2074
+ // prompt either consumes queuedPrompt or races the later deferred fork.
2075
+ // Reject observably while preserving the user's pending setup intact.
2076
+ const setupKind = existing.session.queued ? 'queued' : 'pending setup';
2077
+ throw new Error(`scheduled continuation ${task.id} is blocked by ${setupKind} session ${existing.session.sessionId}; ` +
2078
+ 'the deferred task was preserved');
2079
+ }
2080
+ if (isContinuation && existing) {
1817
2081
  markSessionActivity(existing);
1818
- try {
1819
- ensureSessionWhiteboard(existing);
1820
- if (sharedTopicRootId) {
1821
- beginReplyTargetTurn(existing, sharedTopicRootId, scheduledTurnId);
1822
- sessionStore.updateSession(existing.session);
2082
+ ensureSessionWhiteboard(existing);
2083
+ if (sharedTopicRootId) {
2084
+ beginReplyTargetTurn(existing, sharedTopicRootId, scheduledTurnId);
2085
+ sessionStore.updateSession(existing.session);
2086
+ }
2087
+ const input = buildFollowUpCliInput(firePrompt, existing.session.sessionId, {
2088
+ isAdoptMode: false,
2089
+ cliId: existing.session.cliId ?? bot.config.cliId,
2090
+ cliPathOverride: existing.session.cliPathOverride ?? bot.config.cliPathOverride,
2091
+ locale: localeForBot(larkAppId),
2092
+ larkAppId,
2093
+ chatId: task.chatId,
2094
+ whiteboardId: existing.session.whiteboardId,
2095
+ });
2096
+ rememberLastCliInput(existing, task.prompt, input);
2097
+ if (silent)
2098
+ armSilentScheduledTurn(existing, scheduledTurnId);
2099
+ if (existing.worker && !existing.worker.killed) {
2100
+ try {
2101
+ if (sendWorkerInput(existing, input, scheduledTurnId)) {
2102
+ logger.info(`[scheduler] Task "${task.name}" injected into live session ${existing.session.sessionId}${silent ? ' (silent)' : ''}`);
2103
+ return;
2104
+ }
1823
2105
  }
1824
- const input = buildFollowUpCliInput(firePrompt, existing.session.sessionId, {
1825
- isAdoptMode: false,
1826
- cliId: existing.session.cliId ?? bot.config.cliId,
1827
- cliPathOverride: existing.session.cliPathOverride ?? bot.config.cliPathOverride,
1828
- locale: localeForBot(larkAppId),
1829
- larkAppId,
1830
- chatId: task.chatId,
1831
- whiteboardId: existing.session.whiteboardId,
1832
- });
1833
- rememberLastCliInput(existing, task.prompt, input);
1834
- if (silent)
1835
- armSilentScheduledTurn(existing, scheduledTurnId);
1836
- if (!sendWorkerInput(existing, input, scheduledTurnId)) {
1837
- if (silent)
1838
- disarmSilentScheduledTurn(existing, scheduledTurnId);
1839
- throw new Error('worker unavailable');
2106
+ catch (err) {
2107
+ logger.warn(`[scheduler] Live injection threw (${err.message}); cold-resuming registered session`);
1840
2108
  }
1841
- logger.info(`[scheduler] Task "${task.name}" injected into live session ${existing.session.sessionId}${silent ? ' (silent)' : ''}`);
1842
- return;
2109
+ }
2110
+ if (activeSessions.get(activeKey) !== existing || existing.session.status !== 'active') {
2111
+ if (silent)
2112
+ disarmSilentScheduledTurn(existing, scheduledTurnId);
2113
+ throw new Error(`scheduled continuation lost active session ${existing.session.sessionId}`);
2114
+ }
2115
+ try {
2116
+ forkWorker(existing, input, {
2117
+ resume: existing.hasHistory,
2118
+ turnId: scheduledTurnId,
2119
+ });
1843
2120
  }
1844
2121
  catch (err) {
1845
2122
  if (silent)
1846
2123
  disarmSilentScheduledTurn(existing, scheduledTurnId);
1847
- logger.warn(`[scheduler] Failed to inject into live session (${err.message}); spawning fresh worker`);
2124
+ throw err;
1848
2125
  }
2126
+ logger.info(`[scheduler] Task "${task.name}" cold-resumed session ${existing.session.sessionId}${silent ? ' (silent)' : ''}`);
2127
+ return;
1849
2128
  }
1850
2129
  // Spawn a fresh session bound to the chosen anchor.
1851
2130
  // Thread-scope: rootMessageId = anchor. Chat-scope: rootMessageId stores the
@@ -1893,7 +2172,12 @@ export async function executeScheduledTask(task, activeSessions, refreshCliVersi
1893
2172
  }
1894
2173
  ensureSessionWhiteboard(ds);
1895
2174
  const prompt = buildNewTopicCliInput(firePrompt, session.sessionId, bot.config.cliId, bot.config.cliPathOverride, undefined, undefined, undefined, undefined, { name: bot.botName, openId: bot.botOpenId }, localeForBot(larkAppId), undefined, { larkAppId, chatId: task.chatId, whiteboardId: ds.session.whiteboardId });
1896
- activeSessions.set(sessionKey(anchor, larkAppId), ds);
2175
+ if (!setActiveSessionIfActive(activeSessions, activeKey, ds)) {
2176
+ const winner = activeSessions.get(activeKey);
2177
+ await closeSession(session.sessionId);
2178
+ throw new Error(`scheduled task ${task.id} lost active-session registration for ${anchor}` +
2179
+ (winner ? ` to ${winner.session.sessionId}` : ''));
2180
+ }
1897
2181
  rememberLastCliInput(ds, task.prompt, prompt);
1898
2182
  if (silent)
1899
2183
  armSilentScheduledTurn(ds, scheduledTurnId);
@@ -2031,6 +2315,13 @@ export async function spawnDashboardSession(activeSessions, refreshCliVersion, a
2031
2315
  if (existing && (existing.worker || existing.session.queued || isRelayableRealSession(existing))) {
2032
2316
  return { ok: false, error: 'session_exists' };
2033
2317
  }
2318
+ const quarantinedPersisted = sessionStore.listSessions().find(session => session.status === 'active'
2319
+ && !!session.restoreQuarantinedAt
2320
+ && (session.larkAppId ?? larkAppId) === larkAppId
2321
+ && storedSessionAnchorId(session) === anchor);
2322
+ if (quarantinedPersisted) {
2323
+ return { ok: false, error: 'session_exists' };
2324
+ }
2034
2325
  refreshCliVersion?.(bot.config.cliId, bot.config.cliPathOverride);
2035
2326
  // 可见任务横幅:只由 creator/lead 那次 spawn 发一条,给群成员交代这群是干嘛的。
2036
2327
  // 纯文本、不 @ 任何 bot,不会误触发其它 bot。rootMessageId 存它仅为留痕(chat-scope
@@ -2110,7 +2401,10 @@ export async function spawnDashboardSession(activeSessions, refreshCliVersion, a
2110
2401
  pendingCodexAppMessageContext: codexAppMessageContext,
2111
2402
  pendingAttachments: args.attachments,
2112
2403
  };
2113
- activeSessions.set(sessionKey(anchor, larkAppId), ds);
2404
+ if (!setActiveSessionIfActive(activeSessions, sessionKey(anchor, larkAppId), ds)) {
2405
+ await closeSession(session.sessionId);
2406
+ return { ok: false, error: 'session_exists' };
2407
+ }
2114
2408
  if (column === 'backlog') {
2115
2409
  // Parked:不起 CLI。手动广播 session.spawned,让 dashboard 立刻显示待办池卡片
2116
2410
  // (forkWorker 才会自动发这个事件,parked 路径要自己发)。