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
@@ -3,12 +3,13 @@ import { config } from '../../config.js';
3
3
  import { getBot, getAllBots, getOwnerOpenId } from '../../bot-registry.js';
4
4
  import { canOperate, canTalk } from './event-dispatcher.js';
5
5
  import { updateMessage, deleteMessage, replyMessage, sendMessage, sendUserMessage, sendEphemeralCard, getMessageDetail, isHumanOpenId, resolveUserUnionId as defaultResolveUserUnionId } from './client.js';
6
- import { buildSessionCard, buildStreamingCard, buildTuiPromptCard, buildTuiPromptProcessingCard, buildTuiPromptResolvedCard, buildGrantResultCard, buildGrantNotifyCard, getCliDisplayName, truncateContent, buildConfigCard, buildConfigTextCard, CONFIG_UNSET, buildRepoSelectCard } from './card-builder.js';
7
- import { findConfigField, applyConfigField, coerceConfigValue, getConfigCardData } from '../../services/bot-config-store.js';
6
+ import { buildSessionCard, buildStreamingCard, buildTuiPromptCard, buildTuiPromptProcessingCard, buildGrantResultCard, getCliDisplayName, truncateContent, buildConfigCard, buildConfigTextCard, CONFIG_UNSET, buildRepoSelectCard } from './card-builder.js';
7
+ import { findConfigField, applyConfigField, coerceConfigValue, getConfigCardData, } from '../../services/bot-config-store.js';
8
8
  import { updateBotGrantPrefs } from '../../services/grant-prefs-store.js';
9
9
  import { writeTeamRoleFile, deleteTeamRoleFile } from '../../core/role-resolver.js';
10
10
  import { addChatGrant, addGlobalGrant } from '../../services/grant-store.js';
11
- import { checkNonce, clearPending, markDenied, getPendingQuota, getPendingMessage } from './grant-pending.js';
11
+ import { checkNonce, clearPending, getPendingGrantLimits, getPendingMessage, markDenied, updatePendingGrantLimits, } from './grant-pending.js';
12
+ import { normalizeGrantDurationOption, normalizeGrantQuotaOption } from '../../services/grant-policy.js';
12
13
  import { claimOverloadNonce, releaseOverloadNonce } from './overload-nonce.js';
13
14
  import { buildOverloadAlertCard, buildOverloadExpiredCard, OVERLOAD_ACTION_CLEAN_STOPPED, OVERLOAD_ACTION_SUSPEND_IDLE, OVERLOAD_ACTION_NOOP, } from '../../core/host-overload-alert.js';
14
15
  import { listOnlineDaemons } from '../../utils/daemon-discovery.js';
@@ -27,11 +28,12 @@ import { ttadkConfigModelChoices } from '../../setup/cli-selection.js';
27
28
  import { logger } from '../../utils/logger.js';
28
29
  import * as sessionStore from '../../services/session-store.js';
29
30
  import { loadFrozenCards, saveFrozenCards } from '../../services/frozen-card-store.js';
30
- import { forkWorker, sendWorkerInput, killWorker, scheduleCardPatch, parkStreamCard, clearUsageLimitState, cardUsageLimit, writableTerminalLinkFor, resolvePrivateCardAudience, deliverWriteLinkCard, deliverEphemeralOrReply, CARD_POSTING_SENTINEL, requestSessionRestart } from '../../core/worker-pool.js';
31
+ import { forkWorker, sendWorkerInput, sendWorkerSessionInput, killWorker, closeSession, teardownAuthoritativePersistentBackingBeforeClose, scheduleCardPatch, parkStreamCard, clearUsageLimitState, cardUsageLimit, writableTerminalLinkFor, workerHasInitialized, sessionSupportsWebTerminal, readableTerminalUrlFor, resolvePrivateCardAudience, deliverWriteLinkCard, deliverEphemeralOrReply, CARD_POSTING_SENTINEL, requestSessionRestart, isSessionTransferring, getDaemonStreamingCardUsageSnapshot } from '../../core/worker-pool.js';
31
32
  import { getSessionWorkingDir, buildNewTopicCliInput, getAvailableBots, persistStreamCardState, resumeSession, rememberLastCliInput, ensureSessionWhiteboard } from '../../core/session-manager.js';
32
33
  import { markInitialUserTurnPending } from '../../core/initial-user-turn.js';
33
- import { publishAttentionPatch, announcePendingRepoSession } from '../../core/session-activity.js';
34
+ import { publishAttentionPatch, publishClosedSessionPatch, announcePendingRepoSession } from '../../core/session-activity.js';
34
35
  import { fallbackTurnId } from '../../core/reply-target.js';
36
+ import { sendWorkerIpc } from '../../core/worker-ipc.js';
35
37
  import { validateWorkingDir } from '../../core/working-dir.js';
36
38
  import { sessionKey, sessionAnchorId, frozenDisplayMode, markRepoCardConsumed, isActiveRepoCard, claimCurrentRepoCard } from '../../core/types.js';
37
39
  import { buildTerminalUrl } from '../../core/terminal-url.js';
@@ -402,6 +404,23 @@ opts) {
402
404
  }
403
405
  else {
404
406
  // Mid-session repo switch — close old session, start fresh.
407
+ // ZMX close is identity/generation verified and may refuse. Prove teardown
408
+ // before claiming the card or mutating any old-session state; on refusal
409
+ // the current session and card remain fully retryable.
410
+ try {
411
+ teardownAuthoritativePersistentBackingBeforeClose(ds);
412
+ }
413
+ catch (err) {
414
+ const reason = err instanceof Error ? err.message : String(err);
415
+ logger.warn(`[${tag(ds)}] Repo switch refused because backing teardown was not proven: ${reason}`);
416
+ try {
417
+ await sessionReply(rootId, t('cmd.repo.switch_close_failed', { error: reason }, locTarget));
418
+ }
419
+ catch (replyErr) {
420
+ logger.warn(`[${tag(ds)}] Repo-switch teardown failure reply failed: ${replyErr instanceof Error ? replyErr.message : replyErr}`);
421
+ }
422
+ return;
423
+ }
405
424
  // Claim the current card BEFORE killWorker / any await. A concurrent click
406
425
  // on the same Feishu card must not pass a second kill+fork while the first
407
426
  // switch is still awaiting confirm replies. Correctness does not depend on
@@ -423,6 +442,7 @@ opts) {
423
442
  // the displaced session's stored workingDir (and the closed card), so
424
443
  // `claude --resume` later would reopen the old context in the new repo's
425
444
  // cwd. The new repo is pinned onto the fresh session below instead.
445
+ const oldSession = ds.session;
426
446
  const closedCard = buildClosedSessionCard(ds, locTarget);
427
447
  killWorker(ds);
428
448
  // Park the current card in `frozenCards` so the next POST under the new
@@ -433,8 +453,15 @@ opts) {
433
453
  // stays in the thread instead of vanishing prematurely.
434
454
  parkStreamCard(ds);
435
455
  sessionStore.closeSession(ds.session.sessionId);
456
+ publishClosedSessionPatch(ds.session.sessionId, ds.session.closedAt ? Date.parse(ds.session.closedAt) : undefined);
436
457
  await deliverEphemeralOrReply(ds, operatorOpenId, closedCard, 'interactive', () => sessionReply(rootId, closedCard, 'interactive'));
437
- const oldSession = ds.session;
458
+ // The close card delivery yields to inbound routing and dashboard IPC. If
459
+ // the user closed/replaced this generation during that await, do not mint a
460
+ // fresh active row from the stale continuation.
461
+ if (!sessionStillActive() || ds.session !== oldSession) {
462
+ logger.warn(`[${tag(ds)}] Repo switch cancelled after old-session close; routing generation changed during card delivery`);
463
+ return;
464
+ }
438
465
  // `rootId` is the routing anchor. For chat-scope sessions it is the
439
466
  // `oc_...` chat id, not the traceable `om_...` message root stored on
440
467
  // Session. Preserve the old identity and explicitly persist scope so card
@@ -519,6 +546,18 @@ export async function runAutoWorktreeCommit(deps) {
519
546
  const wt = await maybeCreateDefaultWorktree(larkAppId, baseDir, {
520
547
  isBotDefaultDir: true, title, prompt, locale: localeForBot(larkAppId), notify,
521
548
  });
549
+ // The pendingRepo placeholder can legitimately be consumed WHILE this
550
+ // up-to-30s build runs — e.g. the Codex-notifier「继续处理」callback adopts
551
+ // the same DM session and clears pendingRepo before we get here. If we now
552
+ // funneled into commitRepoSelection, its mid-session branch would kill the
553
+ // freshly-adopted worker and replace it with a botmux-owned worktree
554
+ // session. Bail on the late result instead: the takeover already owns the
555
+ // session. (commitRepoSelection also re-checks pendingRepo under its claim,
556
+ // but that check runs after an await — fence here before any mutation.)
557
+ if (!ds.pendingRepo) {
558
+ logger.info(`[${tag(ds)}] auto-worktree completion ignored — pendingRepo already consumed (session taken over)`);
559
+ return;
560
+ }
522
561
  // Commit even on fallback (wt.dir === baseDir) — the session must still start.
523
562
  // commitRepoSelection has its own /close + generation guards and, for a
524
563
  // pendingRepo session, folds any messages buffered during creation (pendingPrompt
@@ -546,17 +585,19 @@ export async function runAutoWorktreeCommit(deps) {
546
585
  /**
547
586
  * Drive a host-overload降压 sweep across daemons.
548
587
  *
549
- * `suspend_idle` fans out to EVERY online daemon and sums the affected counts
550
- * live workers only exist in their owning daemon's process, so each must sweep
551
- * its own. `clean_stopped` operates on the SHARED, machine-wide session store,
552
- * so ONE daemon does the whole job: it tries daemons in order until the first
553
- * one succeeds (not a fixed `daemons[0]`, so a single flaky descriptor doesn't
554
- * sink the action while healthy siblings sit idle). Fanning clean_stopped out
555
- * would make siblings race the same zombies and double-count them.
588
+ * Both modes fan out to EVERY online daemon and sum the affected counts. Each
589
+ * daemon owns one bot-scoped session store and its own live workers, so neither
590
+ * stopped sessions nor idle workers can be swept through a sibling daemon.
556
591
  *
557
- * Throws when there are no online daemons, or when every attempt failed so
558
- * the caller rolls back the nonce instead of burning the button on an action
559
- * that never ran. A partial success (≥1 daemon ok) returns normally.
592
+ * Fail-closed on partial failure: if ANY discovered daemon doesn't ACK, throw
593
+ * the caller then rolls back the nonce (releaseOverloadNonce) so the owner can
594
+ * retry, instead of burning the button on a card that reports「已清理 0」while
595
+ * the daemon that actually held the zombies never ran. Both sweeps are
596
+ * idempotent (an already-closed session leaves the stopped set; an already-
597
+ * suspended worker leaves the idle set), so a retry only re-hits whatever
598
+ * failed. Reporting a partial count as a completed action would resurrect the
599
+ * exact "显示 0、实际没清" symptom this fix exists to kill — just triggered by
600
+ * "the daemon holding the zombies failed" instead of "the first daemon had none".
560
601
  */
561
602
  async function sweepHostOverload(mode) {
562
603
  const daemons = listOnlineDaemons();
@@ -580,35 +621,20 @@ async function sweepHostOverload(mode) {
580
621
  return null;
581
622
  }
582
623
  };
583
- if (mode === 'clean_stopped') {
584
- // Machine-wide (shared store): try daemons in order, stop at the first
585
- // success. Only if EVERY daemon failed do we throw (→ nonce rollback).
586
- for (const d of daemons) {
587
- const n = await postSweep(d);
588
- if (n !== null)
589
- return n;
590
- }
591
- throw new Error(`sweep clean_stopped reached no daemon (${daemons.length} failed)`);
592
- }
593
- // suspend_idle: fan out to all daemons and sum. Throw only if not one acked.
594
- let affected = 0;
595
- let ok = 0;
624
+ // Fan out to all daemons and sum. Fail the whole action if ANY didn't ACK, so
625
+ // the caller keeps the button retriable rather than reporting a partial sweep
626
+ // as complete.
596
627
  const results = await Promise.all(daemons.map(postSweep));
597
- for (const n of results) {
598
- if (n !== null) {
599
- affected += n;
600
- ok++;
601
- }
628
+ const failed = results.filter(n => n === null).length;
629
+ if (failed > 0) {
630
+ throw new Error(`sweep ${mode}: ${failed}/${daemons.length} daemon(s) did not ack — retriable`);
602
631
  }
603
- if (ok === 0)
604
- throw new Error(`sweep suspend_idle reached no daemon (${daemons.length} failed)`);
605
- return affected;
632
+ return results.reduce((sum, n) => sum + (n ?? 0), 0);
606
633
  }
607
634
  /**
608
- * Machine-wide counts for the overload alert preview. `stopped` (zombies) reads
609
- * the SHARED session store, so every daemon returns the same number take the
610
- * max (any one authoritative). `idle` live workers are per-owning-daemon sum.
611
- * Best-effort: an unreachable daemon just contributes 0.
635
+ * Machine-wide counts for the overload alert preview. Each daemon reports its
636
+ * bot-scoped stopped sessions and its own live workers, so both fields must be
637
+ * summed. Best-effort: an unreachable daemon just contributes 0.
612
638
  */
613
639
  export async function countHostOverload() {
614
640
  const daemons = listOnlineDaemons();
@@ -620,7 +646,7 @@ export async function countHostOverload() {
620
646
  const body = await res.json().catch(() => ({}));
621
647
  if (res.ok && body?.ok) {
622
648
  if (typeof body.stopped === 'number')
623
- stopped = Math.max(stopped, body.stopped);
649
+ stopped += body.stopped;
624
650
  if (typeof body.idle === 'number')
625
651
  idle += body.idle;
626
652
  }
@@ -710,9 +736,13 @@ export async function handleCardAction(data, deps, larkAppId) {
710
736
  // Rebuild the SAME card: clicked button → ✓done+disabled, other → still live.
711
737
  return JSON.parse(buildOverloadAlertCard(st));
712
738
  }
713
- // ─── 群内授权卡片动作(grant_chat / grant_global / grant_deny,talk-only)─────
739
+ // ─── 群内授权卡片动作(限制提交 + grant/revoke,talk-only)──────────────────
714
740
  // 不绑定 session,必须在 session 解析之前处理。owner 强闸门 + nonce 校验。
715
- if (value?.action && (value.action === 'grant_chat' || value.action === 'grant_global' || value.action === 'grant_deny') && larkAppId) {
741
+ if (value?.action && (value.action === 'grant_chat'
742
+ || value.action === 'grant_global'
743
+ || value.action === 'grant_deny'
744
+ || value.action === 'grant_set_duration'
745
+ || value.action === 'grant_set_quota') && larkAppId) {
716
746
  const loc = localeForBot(larkAppId);
717
747
  const owner = getOwnerOpenId(larkAppId);
718
748
  // owner 强闸门:必须是当前 app 的 owner 本人(比 canOperate 更严)
@@ -731,6 +761,23 @@ export async function handleCardAction(data, deps, larkAppId) {
731
761
  if (!targets.length || !grantChatId || !nonce || !targets.every(tt => checkNonce(larkAppId, grantChatId, tt, nonce))) {
732
762
  return { toast: { type: 'error', content: t('card.grant.toast_expired', undefined, loc) } };
733
763
  }
764
+ // 兼容已发出的旧卡:旧卡用下拉 callback 暂存限制,新卡在授权按钮的 form_submit
765
+ // 中一次提交有效期和额度。两条路径都必须走服务端校验,不能信任卡片输入。
766
+ if (value.action === 'grant_set_duration' || value.action === 'grant_set_quota') {
767
+ const selected = action?.option;
768
+ const parsed = value.action === 'grant_set_duration'
769
+ ? normalizeGrantDurationOption(selected)
770
+ : normalizeGrantQuotaOption(selected);
771
+ if (parsed === null) {
772
+ return { toast: { type: 'error', content: t('card.grant.toast_bad_limit', undefined, loc) } };
773
+ }
774
+ const updated = updatePendingGrantLimits(larkAppId, grantChatId, targets, nonce, value.action === 'grant_set_duration'
775
+ ? { durationMs: parsed ?? null }
776
+ : { quota: parsed ?? null });
777
+ return updated
778
+ ? { toast: { type: 'success', content: t('card.grant.toast_limit_staged', undefined, loc) } }
779
+ : { toast: { type: 'error', content: t('card.grant.toast_expired', undefined, loc) } };
780
+ }
734
781
  // 拒绝:只把卡更新成「已拒绝」+ 全部目标进 deny 冷却,绝不触碰 grant-store。
735
782
  // 返回原始卡 body,由 dispatcher 包成 in-place patch(不再走 updateMessage 双写)。
736
783
  if (value.action === 'grant_deny') {
@@ -738,14 +785,36 @@ export async function handleCardAction(data, deps, larkAppId) {
738
785
  markDenied(larkAppId, grantChatId, tt);
739
786
  return JSON.parse(buildGrantResultCard('deny', loc));
740
787
  }
788
+ const formValue = action?.form_value;
789
+ if (formValue) {
790
+ const duration = normalizeGrantDurationOption(formValue.grant_duration);
791
+ const quota = normalizeGrantQuotaOption(typeof formValue.grant_quota === 'string' ? formValue.grant_quota.trim() : formValue.grant_quota);
792
+ if (quota === null) {
793
+ return { toast: { type: 'error', content: t('card.grant.toast_bad_quota', undefined, loc) } };
794
+ }
795
+ if (duration === null) {
796
+ return { toast: { type: 'error', content: t('card.grant.toast_bad_limit', undefined, loc) } };
797
+ }
798
+ if (!updatePendingGrantLimits(larkAppId, grantChatId, targets, nonce, {
799
+ durationMs: duration ?? null,
800
+ quota: quota ?? null,
801
+ })) {
802
+ return { toast: { type: 'error', content: t('card.grant.toast_expired', undefined, loc) } };
803
+ }
804
+ }
741
805
  // 授权(talk-only):grant_chat 写本群 chatGrants,grant_global 写全局 globalGrants,
742
806
  // 两者都绝不碰 allowedUsers(operate 只由 bots.json 配)。逐个落库,统计成功/失败。
743
807
  const kind = value.action === 'grant_global' ? 'global' : 'chat';
744
808
  const names = Array.isArray(value.target_names) ? value.target_names : [];
745
809
  const idToName = new Map();
746
810
  targets.forEach((tt, i) => idToName.set(tt, names[i] ?? ''));
747
- // 额度挂在 pending 上(/grant @x N 解析所得;多目标共用同一额度);clearPending 前先读出来。
748
- const quota = getPendingQuota(larkAppId, grantChatId, targets[0]);
811
+ // 限制挂在 pending 上;确认时生成绝对 expiresAt,多目标共享同一组限制。
812
+ const limits = getPendingGrantLimits(larkAppId, grantChatId, targets[0]);
813
+ if (!limits) {
814
+ return { toast: { type: 'error', content: t('card.grant.toast_expired', undefined, loc) } };
815
+ }
816
+ const quota = limits.quota;
817
+ const expiresAt = limits.durationMs === undefined ? undefined : Date.now() + limits.durationMs;
749
818
  // 触发本次授权申请的原始消息事件:授权成功后重放,用户无需再 @ 一遍。
750
819
  // 自助申请卡每次只对应一个 target,取第一个即可。
751
820
  const pendingMessage = getPendingMessage(larkAppId, grantChatId, targets[0]);
@@ -753,8 +822,8 @@ export async function handleCardAction(data, deps, larkAppId) {
753
822
  const failed = [];
754
823
  for (const tt of targets) {
755
824
  const res = kind === 'global'
756
- ? await addGlobalGrant(larkAppId, tt, quota)
757
- : await addChatGrant(larkAppId, grantChatId, tt, quota);
825
+ ? await addGlobalGrant(larkAppId, tt, quota, expiresAt)
826
+ : await addChatGrant(larkAppId, grantChatId, tt, quota, expiresAt);
758
827
  if (res.ok) {
759
828
  clearPending(larkAppId, grantChatId, tt);
760
829
  granted.push(tt);
@@ -801,12 +870,12 @@ export async function handleCardAction(data, deps, larkAppId) {
801
870
  }
802
871
  // 通知卡的 grantee 渲染参数:bot 只用纯文本名字(不 <at>,否则唤醒对方 bot 误拉空会话),真人 @ 点名。
803
872
  const notifyTargets = granted.map((id, i) => ({ openId: id, name: idToName.get(id) || undefined, isBot: !humanFlags[i] }));
804
- // 授权成功后:
805
- // 1. 先同步返回 callback 响应(in-place patch 成「已授权」终态卡),避免飞书等待
806
- // 太久或 deleteMessage callback 响应竞态导致客户端 300000 报错;
807
- // 2. 通知卡 + 部分失败告知 + 撤回原卡 走后台 fire-and-forget(不阻塞 callback)。
808
- const resultCardBody = JSON.parse(buildGrantResultCard(kind, loc));
809
- if (cardMessageId) {
873
+ // 授权成功后:**就地 patch 原卡**为终态(正文直接 @ 被授权人 + 额度/有效期),这一张卡既是
874
+ // 「已授权」结果态、又 ping 到被授权人——无需再单独发通知卡、也无需撤回原卡(申晗 2026-07-31
875
+ // 反馈:直接在原卡更新即可)。同步返回该 body 即完成 in-place patch,避免 deleteMessage 与
876
+ // callback 响应竞态导致客户端 300000。仅「部分失败」仍走后台补一条文字告知。
877
+ const resultCardBody = JSON.parse(buildGrantResultCard(kind, loc, quota, expiresAt, notifyTargets));
878
+ if (cardMessageId && failed.length > 0) {
810
879
  let replyInThread = true;
811
880
  try {
812
881
  const detail = await getMessageDetail(larkAppId, cardMessageId);
@@ -816,31 +885,17 @@ export async function handleCardAction(data, deps, larkAppId) {
816
885
  replyInThread = Boolean(item.thread_id);
817
886
  }
818
887
  catch (err) {
819
- logger.debug(`grant notify thread-mode probe failed, defaulting to thread reply: ${err}`);
888
+ logger.debug(`grant partial-failure thread-mode probe failed, defaulting to thread reply: ${err}`);
820
889
  }
821
- // fire-and-forget: 通知卡 + 部分失败文字 + 撤回原卡
890
+ // fire-and-forget: 仅在有部分目标失败时补一条文字告知(不阻塞 callback)。
822
891
  Promise.resolve()
823
892
  .then(async () => {
893
+ const failNames = failed.map(f => idToName.get(f.openId) || f.openId).join('、');
824
894
  try {
825
- await replyMessage(larkAppId, cardMessageId, buildGrantNotifyCard(kind, notifyTargets, loc, quota), 'interactive', replyInThread);
895
+ await replyMessage(larkAppId, cardMessageId, t('card.grant.partial_failed', { names: failNames }, loc), 'text', replyInThread);
826
896
  }
827
897
  catch (err) {
828
- logger.warn(`grant notify failed (grant still applied): ${err}`);
829
- }
830
- if (failed.length > 0) {
831
- const failNames = failed.map(f => idToName.get(f.openId) || f.openId).join('、');
832
- try {
833
- await replyMessage(larkAppId, cardMessageId, t('card.grant.partial_failed', { names: failNames }, loc), 'text', replyInThread);
834
- }
835
- catch (err) {
836
- logger.warn(`grant partial-failure notice failed: ${err}`);
837
- }
838
- }
839
- try {
840
- await deleteMessage(larkAppId, cardMessageId);
841
- }
842
- catch (err) {
843
- logger.debug(`grant card withdraw (post-callback) failed: ${err}`);
898
+ logger.warn(`grant partial-failure notice failed: ${err}`);
844
899
  }
845
900
  })
846
901
  .catch(err => logger.error(`grant post-callback background tasks failed: ${err}`));
@@ -966,6 +1021,7 @@ export async function handleCardAction(data, deps, larkAppId) {
966
1021
  // session's chatType for DM targets. Default 'group' covers legacy cards.
967
1022
  const targetScope = value.target_scope ?? 'chat';
968
1023
  const targetChatType = value.target_chat_type ?? 'group';
1024
+ const cardVisibility = value.visibility ?? 'public';
969
1025
  const invokerOpenId = value.invoker_open_id;
970
1026
  if (!targetChatId || !targetRootId || !operatorOpenId) {
971
1027
  return { toast: { type: 'error', content: t('card.relay.toast_failed', { error: 'missing_value' }, loc) } };
@@ -1018,7 +1074,32 @@ export async function handleCardAction(data, deps, larkAppId) {
1018
1074
  selectedSessionId: nextSelected,
1019
1075
  searchQuery: nextSearch,
1020
1076
  page: nextPage,
1021
- }, targetScope, targetChatType);
1077
+ }, targetScope, targetChatType, cardVisibility);
1078
+ // ── Private (ephemeral) picker: delete-then-resend ────────────────────
1079
+ // Ephemeral cards CANNOT be PATCH-updated (Feishu legacy interface), so we
1080
+ // can't return a body for the dispatcher to patch in place. Instead delete
1081
+ // the clicked card and resend a fresh ephemeral one carrying the new state
1082
+ // — the same send→interact→delete→resend pattern Feishu documents for
1083
+ // ephemeral flows. Resend FIRST, then delete the old one, so a resend
1084
+ // failure doesn't leave the user with no card at all. The toast keeps the
1085
+ // callback response non-empty (returning {} would make Lark show nothing).
1086
+ if (cardVisibility === 'private') {
1087
+ const { sendEphemeralCard, deleteEphemeralCard } = await import('./client.js');
1088
+ try {
1089
+ await sendEphemeralCard(larkAppId, targetChatId, invokerOpenId ?? operatorOpenId, cardJson);
1090
+ if (cardMessageId) {
1091
+ await deleteEphemeralCard(larkAppId, cardMessageId).catch(() => { });
1092
+ }
1093
+ // No card body returned — the replacement is already sent. An empty
1094
+ // response is fine here (the new ephemeral card is the user-visible
1095
+ // result); a toast would double up with the freshly-rendered card.
1096
+ return;
1097
+ }
1098
+ catch (err) {
1099
+ logger.warn(`[card-action] relay private picker re-render failed (${err instanceof Error ? err.message : err}); leaving old card in place`);
1100
+ return { toast: { type: 'error', content: t('card.relay.toast_failed', { error: 'ephemeral_refresh_failed' }, loc) } };
1101
+ }
1102
+ }
1022
1103
  // Return an updated card body — event-dispatcher wraps this as
1023
1104
  // { card: { type: 'raw', data: <body> } } so Lark patches the picker
1024
1105
  // in place rather than appending a new message.
@@ -1167,6 +1248,7 @@ export async function handleCardAction(data, deps, larkAppId) {
1167
1248
  // field and default to 'group' (their pickers never offered DM targets).
1168
1249
  const targetScope = value.target_scope ?? 'chat';
1169
1250
  const targetChatType = value.target_chat_type ?? 'group';
1251
+ const cardVisibility = value.visibility ?? 'public';
1170
1252
  const targetAnchor = targetScope === 'chat' ? targetChatId : targetRootId;
1171
1253
  const invokerOpenId = value.invoker_open_id;
1172
1254
  if (!sourceSessionId || !targetChatId || !targetRootId) {
@@ -1208,22 +1290,20 @@ export async function handleCardAction(data, deps, larkAppId) {
1208
1290
  // collectRelayPickerEntries already filters scratches at render time,
1209
1291
  // but a stale picker (rendered before a scratch was created) could
1210
1292
  // still produce a confirm click; this is the depth defense.
1211
- {
1212
- const { isRelayableRealSession } = await import('../../core/worker-pool.js');
1213
- if (!isRelayableRealSession(sourceDs)) {
1214
- return { toast: { type: 'error', content: t('card.relay.toast_not_started_yet', undefined, loc) } };
1215
- }
1293
+ const { isDisposableCommandScratch, isRelayableRealSession } = await import('../../core/worker-pool.js');
1294
+ if (!isRelayableRealSession(sourceDs)) {
1295
+ return { toast: { type: 'error', content: t('card.relay.toast_not_started_yet', undefined, loc) } };
1216
1296
  }
1217
1297
  // Pre-flight target-chat conflict check — done BEFORE sendMessage M1 so
1218
1298
  // a refusal doesn't leave a misleading "已接力" announcement in the
1219
1299
  // target chat (王皓 caught this in testing). Mirror the same predicate
1220
- // transferSession uses, plus the `!!worker` filter that excludes daemon
1221
- // command scratch sessions (e.g. the /relay command's own session,
1222
- // which shares the bot's larkAppId + chatId but has no worker).
1300
+ // transferSession uses. Only a narrowly classified daemon-command scratch
1301
+ // may be ignored; worker-less queued/adopt/deferred/real sessions still own
1302
+ // the anchor and must block before a misleading M1 is sent.
1223
1303
  const targetConflict = [...activeSessions.values()].find(c => c !== sourceDs
1224
1304
  && c.larkAppId === larkAppId
1225
1305
  && sessionAnchorId(c) === targetAnchor
1226
- && !!c.worker);
1306
+ && !isDisposableCommandScratch(c));
1227
1307
  if (targetConflict) {
1228
1308
  const conflictTitle = targetConflict.session.title || targetConflict.session.sessionId.substring(0, 8);
1229
1309
  // Send as a regular text message in the target chat instead of a
@@ -1291,8 +1371,16 @@ export async function handleCardAction(data, deps, larkAppId) {
1291
1371
  return { toast: { type: 'error', content: t('card.relay.toast_failed', { error: r.error }, loc) } };
1292
1372
  }
1293
1373
  // Best-effort: remove the picker card now that the selection resolved.
1374
+ // Ephemeral (private) pickers need the ephemeral-delete endpoint —
1375
+ // deleteMessage (im/v1/messages) doesn't apply to ephemeral message ids.
1294
1376
  if (cardMessageId && larkAppId) {
1295
- deleteMessage(larkAppId, cardMessageId).catch(() => { });
1377
+ if (cardVisibility === 'private') {
1378
+ const { deleteEphemeralCard } = await import('./client.js');
1379
+ deleteEphemeralCard(larkAppId, cardMessageId).catch(() => { });
1380
+ }
1381
+ else {
1382
+ deleteMessage(larkAppId, cardMessageId).catch(() => { });
1383
+ }
1296
1384
  }
1297
1385
  return { toast: { type: 'success', content: t('card.relay.toast_success', undefined, loc) } };
1298
1386
  }
@@ -1562,6 +1650,14 @@ export async function handleCardAction(data, deps, larkAppId) {
1562
1650
  await sessionReply(rootId, t('card.action.adopt_no_restart', undefined, locDs));
1563
1651
  return;
1564
1652
  }
1653
+ if (isSessionTransferring(ds)) {
1654
+ return {
1655
+ toast: {
1656
+ type: 'warning',
1657
+ content: t('cmd.session.transfer_in_progress', undefined, locDs),
1658
+ },
1659
+ };
1660
+ }
1565
1661
  const effectiveCliId = sessionCliId(ds);
1566
1662
  const cliName = getCliDisplayName(effectiveCliId);
1567
1663
  logger.info(`[${tag(ds)}] Correlated restart via card button`);
@@ -1583,9 +1679,23 @@ export async function handleCardAction(data, deps, larkAppId) {
1583
1679
  // Build the closed card BEFORE killWorker/closeSession — it reads the
1584
1680
  // live session's identity off `ds`.
1585
1681
  const card = buildClosedSessionCard(ds, localeForBot(ds.larkAppId));
1586
- killWorker(ds);
1587
- sessionStore.closeSession(ds.session.sessionId);
1588
- activeSessions.delete(sKey);
1682
+ try {
1683
+ await closeSession(ds.session.sessionId);
1684
+ }
1685
+ catch (err) {
1686
+ logger.error(`[${tag(ds)}] Refused close because backing teardown was not verified: ${err}`);
1687
+ return {
1688
+ toast: {
1689
+ type: 'warning',
1690
+ content: `会话关闭失败:${err instanceof Error ? err.message : String(err)}`,
1691
+ },
1692
+ };
1693
+ }
1694
+ // closeSession removes the captured ds before awaiting best-effort remote
1695
+ // cleanup. A new session may claim the same route during that await; only
1696
+ // delete here if this handler still owns the exact object it closed.
1697
+ if (activeSessions.get(sKey) === ds)
1698
+ activeSessions.delete(sKey);
1589
1699
  // The closed card carries session title / CLI name / workingDir / resume
1590
1700
  // command. In private-card mode those must not leak to the group — send the
1591
1701
  // closed card ephemeral to the same owner audience instead. No group
@@ -1637,11 +1747,15 @@ export async function handleCardAction(data, deps, larkAppId) {
1637
1747
  else if (result.error === 'deferred_unmaterialized') {
1638
1748
  await sessionReply(rootId, t('card.action.resume_deferred_unmaterialized', undefined, locDsResume));
1639
1749
  }
1750
+ else if (result.error === 'resume_cancelled') {
1751
+ await sessionReply(rootId, t('card.action.resume_cancelled', undefined, locDsResume));
1752
+ }
1640
1753
  }
1641
1754
  }
1642
1755
  if (actionType === 'disconnect' && ds) {
1643
1756
  killWorker(ds);
1644
1757
  sessionStore.closeSession(ds.session.sessionId);
1758
+ publishClosedSessionPatch(ds.session.sessionId, ds.session.closedAt ? Date.parse(ds.session.closedAt) : undefined);
1645
1759
  activeSessions.delete(sKey);
1646
1760
  await sessionReply(rootId, t('card.action.disconnected', undefined, localeForBot(ds.larkAppId)));
1647
1761
  logger.info(`[${tag(ds)}] Disconnected (adopt) via card button`);
@@ -1672,9 +1786,9 @@ export async function handleCardAction(data, deps, larkAppId) {
1672
1786
  ds.currentImageKey = undefined;
1673
1787
  persistStreamCardState(ds);
1674
1788
  let cardJson;
1675
- if (ds.streamCardId && ds.streamCardId !== CARD_POSTING_SENTINEL && ds.workerPort) {
1676
- const readUrl = buildTerminalUrl(ds);
1677
- cardJson = buildStreamingCard(ds.session.sessionId, sessionAnchorId(ds), readUrl, ds.currentTurnTitle, '', 'working', sessionCliId(ds), ds.displayMode ?? 'hidden', ds.streamCardNonce, ds.currentImageKey, !!ds.adoptedFrom, false, locDs, undefined, writableTerminalLinkFor(ds), isLocalCliOpenReady(ds, { cliId: sessionCliId(ds) }));
1789
+ if (ds.streamCardId && ds.streamCardId !== CARD_POSTING_SENTINEL && workerHasInitialized(ds)) {
1790
+ const readUrl = readableTerminalUrlFor(ds);
1791
+ cardJson = buildStreamingCard(ds.session.sessionId, sessionAnchorId(ds), readUrl, ds.currentTurnTitle, '', 'working', sessionCliId(ds), ds.displayMode ?? 'hidden', ds.streamCardNonce, ds.currentImageKey, !!ds.adoptedFrom, false, locDs, undefined, writableTerminalLinkFor(ds), isLocalCliOpenReady(ds, { cliId: sessionCliId(ds) }), getDaemonStreamingCardUsageSnapshot(ds, sessionCliId(ds)));
1678
1792
  scheduleCardPatch(ds, cardJson);
1679
1793
  }
1680
1794
  const retryCodexAppInput = ds.lastCodexAppInput
@@ -1698,6 +1812,14 @@ export async function handleCardAction(data, deps, larkAppId) {
1698
1812
  return;
1699
1813
  }
1700
1814
  if (actionType === 'tui_keys' && ds) {
1815
+ if (isSessionTransferring(ds)) {
1816
+ return {
1817
+ toast: {
1818
+ type: 'warning',
1819
+ content: t('cmd.session.transfer_in_progress', undefined, localeForBot(ds.larkAppId)),
1820
+ },
1821
+ };
1822
+ }
1701
1823
  // Fail-closed: only act on a currently-active card (either the
1702
1824
  // ScreenAnalyzer TUI prompt card or our stuck-warning card). A stale
1703
1825
  // click from a resolved/replaced card must NOT send any IPC to the
@@ -1718,6 +1840,10 @@ export async function handleCardAction(data, deps, larkAppId) {
1718
1840
  const optionType = value?.option_type ?? 'select';
1719
1841
  const selectedIndex = Number(value?.selected_index ?? 0);
1720
1842
  const selectedText = value?.selected_text ?? `Option ${selectedIndex + 1}`;
1843
+ if (isActiveTuiCard && ds.tuiPromptProcessing) {
1844
+ logger.info(`[${tag(ds)}] Duplicate TUI prompt card click — dropped (processing already in flight)`);
1845
+ return;
1846
+ }
1721
1847
  if (optionType === 'toggle') {
1722
1848
  // Only a ScreenAnalyzer TUI card may own toggle state. A stuck-warning
1723
1849
  // card must never read or mutate the other card's global selections.
@@ -1755,6 +1881,7 @@ export async function handleCardAction(data, deps, larkAppId) {
1755
1881
  if (ds.worker) {
1756
1882
  let allKeys = [];
1757
1883
  let isFinalStuck = false;
1884
+ let dispatchedKeys = false;
1758
1885
  if (isActiveTuiCard && ds.tuiToggledIndices?.length && ds.tuiPromptOptions) {
1759
1886
  // Send each toggled option's keys in sequence
1760
1887
  for (const ti of ds.tuiToggledIndices.sort((a, b) => a - b)) {
@@ -1805,6 +1932,7 @@ export async function handleCardAction(data, deps, larkAppId) {
1805
1932
  allKeys.push(...keys);
1806
1933
  }
1807
1934
  if (allKeys.length > 0) {
1935
+ const effectiveFinal = isFinal || isFinalStuck;
1808
1936
  // Atomic processing claim: if a previous click is already in flight
1809
1937
  // (waiting for tui_keys_delivered / stuck_warning_expired ACK), drop
1810
1938
  // this duplicate. Without this, two rapid clicks could both pass the
@@ -1816,6 +1944,9 @@ export async function handleCardAction(data, deps, larkAppId) {
1816
1944
  }
1817
1945
  ds.stuckWarningProcessing = true;
1818
1946
  }
1947
+ if (isActiveTuiCard && effectiveFinal) {
1948
+ ds.tuiPromptProcessing = true;
1949
+ }
1819
1950
  // Only the stuck-warning card's Enter action re-arms the detector
1820
1951
  // (Enter advances from the hook list to a per-hook review). Match by
1821
1952
  // the actual keys sent — NOT by optionType, since t is typed 'confirm'
@@ -1831,11 +1962,39 @@ export async function handleCardAction(data, deps, larkAppId) {
1831
1962
  const stuckNonce = isActiveStuckCard ? ds.stuckWarningNonce : undefined;
1832
1963
  const stuckCliLifetime = isActiveStuckCard ? ds.stuckWarningCliLifetime : undefined;
1833
1964
  const stuckPage = isActiveStuckCard ? ds.stuckWarningPageType : undefined;
1834
- const effectiveFinal = isFinal || isFinalStuck;
1835
- ds.worker.send({ type: 'tui_keys', keys: allKeys, isFinal: effectiveFinal, rearmStuckDetector: isStuckWarningEnter, stuckNonce, stuckCliLifetime, stuckPageType: stuckPage });
1965
+ const resolveText = isActiveTuiCard && ds.tuiToggledIndices?.length
1966
+ ? ds.tuiToggledIndices.map(i => ds.tuiPromptOptions?.[i]?.text).filter(Boolean).join(', ')
1967
+ : selectedText;
1968
+ try {
1969
+ await sendWorkerIpc(ds.worker, {
1970
+ type: 'tui_keys',
1971
+ keys: allKeys,
1972
+ isFinal: effectiveFinal,
1973
+ rearmStuckDetector: isStuckWarningEnter,
1974
+ stuckNonce,
1975
+ stuckCliLifetime,
1976
+ stuckPageType: stuckPage,
1977
+ cardMessageId: isActiveTuiCard ? cardMessageId : undefined,
1978
+ selectedText: resolveText || selectedText,
1979
+ });
1980
+ dispatchedKeys = true;
1981
+ }
1982
+ catch (err) {
1983
+ if (isActiveStuckCard)
1984
+ ds.stuckWarningProcessing = false;
1985
+ if (isActiveTuiCard && effectiveFinal)
1986
+ ds.tuiPromptProcessing = false;
1987
+ logger.warn(`[${tag(ds)}] TUI key IPC delivery failed: ${err instanceof Error ? err.message : String(err)}`);
1988
+ return {
1989
+ toast: {
1990
+ type: 'warning',
1991
+ content: t('card.action.tui_ipc_failed', undefined, localeForBot(ds.larkAppId)),
1992
+ },
1993
+ };
1994
+ }
1836
1995
  logger.info(`[${tag(ds)}] TUI keys: [${allKeys.join(',')}] final=${effectiveFinal} rearmStuck=${isStuckWarningEnter} stuckNonce=${stuckNonce ?? 'none'} — "${selectedText}"`);
1837
1996
  }
1838
- if (isFinal || isFinalStuck) {
1997
+ if ((isFinal || isFinalStuck) && dispatchedKeys) {
1839
1998
  const resolveText = isActiveTuiCard && ds.tuiToggledIndices?.length
1840
1999
  ? ds.tuiToggledIndices.map(i => ds.tuiPromptOptions?.[i]?.text).filter(Boolean).join(', ')
1841
2000
  : selectedText;
@@ -1844,8 +2003,9 @@ export async function handleCardAction(data, deps, larkAppId) {
1844
2003
  // For a stuck-warning card, do NOT clear authority or render success
1845
2004
  // here. The worker may still reject the keys (stale screen). We show a
1846
2005
  // "processing" state to block duplicate clicks, and wait for the
1847
- // worker's tui_keys_delivered (success) or stuck_warning_expired
1848
- // ("page changed, not sent") ACK before resolving the card.
2006
+ // worker's tui_keys_delivered (success), stuck_warning_expired
2007
+ // ("page changed, not sent"), or tui_prompt_submit_failed ACK before
2008
+ // resolving the card.
1849
2009
  if (isActiveStuckCard) {
1850
2010
  if (cardMessageId) {
1851
2011
  const processingCard = buildTuiPromptProcessingCard('处理中…', locDs);
@@ -1857,22 +2017,9 @@ export async function handleCardAction(data, deps, larkAppId) {
1857
2017
  }
1858
2018
  catch { /* fall through */ }
1859
2019
  }
1860
- // Normal TUI prompt card (ScreenAnalyzer): resolve immediately.
1861
- if (cardMessageId) {
1862
- setTimeout(() => {
1863
- const resolvedCard = buildTuiPromptResolvedCard(finalText, locDs);
1864
- updateMessage(ds.larkAppId, cardMessageId, resolvedCard).catch(err => logger.debug(`[${tag(ds)}] Failed to update TUI prompt card: ${err}`));
1865
- }, allKeys.length * 100 + 500);
1866
- }
1867
- // Clear state only for the card that was actually clicked — a stuck
1868
- // card click must NOT wipe the ScreenAnalyzer TUI prompt state (or
1869
- // vice versa) if both coexist / race.
1870
- if (cardMessageId === ds.tuiPromptCardId) {
1871
- ds.tuiPromptCardId = undefined;
1872
- ds.tuiPromptOptions = undefined;
1873
- ds.tuiPromptMultiSelect = undefined;
1874
- ds.tuiToggledIndices = undefined;
1875
- }
2020
+ // Normal TUI prompt cards also remain in processing until the worker
2021
+ // confirms backend delivery via tui_prompt_resolved. A worker/backend
2022
+ // rejection produces tui_prompt_submit_failed instead.
1876
2023
  publishAttentionPatch(ds);
1877
2024
  try {
1878
2025
  return JSON.parse(buildTuiPromptProcessingCard(finalText, locDs));
@@ -1882,6 +2029,14 @@ export async function handleCardAction(data, deps, larkAppId) {
1882
2029
  }
1883
2030
  }
1884
2031
  if (actionType === 'tui_text_input' && ds) {
2032
+ if (isSessionTransferring(ds)) {
2033
+ return {
2034
+ toast: {
2035
+ type: 'warning',
2036
+ content: t('cmd.session.transfer_in_progress', undefined, localeForBot(ds.larkAppId)),
2037
+ },
2038
+ };
2039
+ }
1885
2040
  const inputTextRaw = action?.form_value?.tui_custom_input;
1886
2041
  const inputText = typeof inputTextRaw === 'string' ? inputTextRaw : '';
1887
2042
  let inputKeys = [];
@@ -1890,20 +2045,49 @@ export async function handleCardAction(data, deps, larkAppId) {
1890
2045
  }
1891
2046
  catch { /* bad json */ }
1892
2047
  const locDs = localeForBot(ds.larkAppId);
1893
- if (ds.worker && inputText && inputKeys.length > 0) {
1894
- // Atomic IPC — worker handles keys + text in one flow to avoid race
1895
- ds.worker.send({ type: 'tui_text_input', keys: inputKeys, text: inputText });
1896
- logger.info(`[${tag(ds)}] TUI text input: "${inputText}" (keys: ${JSON.stringify(inputKeys)})`);
1897
- if (cardMessageId) {
1898
- const resolvedCard = buildTuiPromptResolvedCard(inputText, locDs);
1899
- updateMessage(ds.larkAppId, cardMessageId, resolvedCard).catch(err => logger.debug(`[${tag(ds)}] Failed to update TUI prompt card: ${err}`));
1900
- }
1901
- ds.tuiPromptCardId = undefined;
1902
- ds.tuiPromptOptions = undefined;
1903
- publishAttentionPatch(ds);
2048
+ const isActiveTuiCard = !!ds.tuiPromptCardId && cardMessageId === ds.tuiPromptCardId;
2049
+ if (!isActiveTuiCard) {
2050
+ logger.info(`[${tag(ds)}] tui_text_input from stale card ${cardMessageId} — ignored`);
2051
+ return;
2052
+ }
2053
+ if (ds.tuiPromptProcessing) {
2054
+ logger.info(`[${tag(ds)}] Duplicate TUI text input dropped (processing already in flight)`);
2055
+ return;
1904
2056
  }
2057
+ if (!ds.worker || !inputText || inputKeys.length === 0) {
2058
+ logger.info(`[${tag(ds)}] TUI text input not dispatched ` +
2059
+ `(worker=${!!ds.worker}, text=${!!inputText}, keys=${inputKeys.length})`);
2060
+ return {
2061
+ toast: {
2062
+ type: 'warning',
2063
+ content: t('card.action.tui_ipc_failed', undefined, locDs),
2064
+ },
2065
+ };
2066
+ }
2067
+ // Atomic IPC — worker handles keys + text in one flow to avoid race
2068
+ ds.tuiPromptProcessing = true;
1905
2069
  try {
1906
- return JSON.parse(buildTuiPromptResolvedCard(inputText || t('card.action.tui_custom_input', undefined, locDs), locDs));
2070
+ await sendWorkerIpc(ds.worker, {
2071
+ type: 'tui_text_input',
2072
+ keys: inputKeys,
2073
+ text: inputText,
2074
+ cardMessageId,
2075
+ });
2076
+ }
2077
+ catch (err) {
2078
+ ds.tuiPromptProcessing = false;
2079
+ logger.warn(`[${tag(ds)}] TUI text IPC delivery failed: ${err instanceof Error ? err.message : String(err)}`);
2080
+ return {
2081
+ toast: {
2082
+ type: 'warning',
2083
+ content: t('card.action.tui_ipc_failed', undefined, locDs),
2084
+ },
2085
+ };
2086
+ }
2087
+ logger.info(`[${tag(ds)}] TUI text input: "${inputText}" (keys: ${JSON.stringify(inputKeys)})`);
2088
+ publishAttentionPatch(ds);
2089
+ try {
2090
+ return JSON.parse(buildTuiPromptProcessingCard(inputText, locDs));
1907
2091
  }
1908
2092
  catch { /* fall through */ }
1909
2093
  }
@@ -1923,10 +2107,20 @@ export async function handleCardAction(data, deps, larkAppId) {
1923
2107
  return launchLocalCli(ds, locDs);
1924
2108
  }
1925
2109
  if (actionType === 'get_write_link' && ds && operatorOpenId) {
1926
- const botCfg = getBot(ds.larkAppId).config;
1927
2110
  const effectiveCliId = sessionCliId(ds);
1928
2111
  const locDs = localeForBot(ds.larkAppId);
1929
- if (ds.riffAccessUrl || (ds.workerPort && ds.workerToken)) {
2112
+ if (!sessionSupportsWebTerminal(ds)) {
2113
+ // Old cards can retain a get_write_link callback and stale port/token
2114
+ // fields after the session is restored onto ZMX. This is a permanent
2115
+ // backend capability boundary, not a transient startup delay.
2116
+ const unsupportedCard = JSON.stringify({
2117
+ config: { wide_screen_mode: true },
2118
+ elements: [{ tag: 'markdown', content: t('card.action.terminal_unsupported', undefined, locDs) }],
2119
+ });
2120
+ await deliverEphemeralOrReply(ds, operatorOpenId, unsupportedCard, 'interactive', () => sessionReply(rootId, unsupportedCard, 'interactive'));
2121
+ return;
2122
+ }
2123
+ if (sessionSupportsWebTerminal(ds) && (ds.riffAccessUrl || (ds.workerPort && ds.workerToken))) {
1930
2124
  const writeUrl = buildTerminalUrl(ds, { write: true });
1931
2125
  const cardJson = buildSessionCard(ds.session.sessionId, sessionAnchorId(ds), writeUrl, ds.session.title || getCliDisplayName(effectiveCliId), effectiveCliId, true, // showManageButtons — write-link card includes restart & close
1932
2126
  !!ds.adoptedFrom, // adoptMode — disconnect, never close-the-CLI
@@ -1973,13 +2167,13 @@ export async function handleCardAction(data, deps, larkAppId) {
1973
2167
  const next = nextMode(cur);
1974
2168
  ds.displayMode = next;
1975
2169
  persistStreamCardState(ds);
1976
- if (ds.worker) {
1977
- ds.worker.send({ type: 'set_display_mode', mode: next });
2170
+ if (ds.worker || isSessionTransferring(ds)) {
2171
+ sendWorkerSessionInput(ds, { type: 'set_display_mode', mode: next });
1978
2172
  }
1979
- if (cardMessageId && ds.workerPort) {
1980
- const readUrl = buildTerminalUrl(ds);
2173
+ if (cardMessageId && workerHasInitialized(ds)) {
2174
+ const readUrl = readableTerminalUrlFor(ds);
1981
2175
  const turnTitle = ds.currentTurnTitle || ds.session.title || getCliDisplayName(effectiveCliId);
1982
- const cardJson = buildStreamingCard(ds.session.sessionId, sessionAnchorId(ds), readUrl, turnTitle, ds.lastScreenContent || '', ds.lastScreenStatus || 'working', effectiveCliId, next, ds.streamCardNonce, ds.currentImageKey, !!ds.adoptedFrom, false, localeForBot(ds.larkAppId), cardUsageLimit(ds), writableTerminalLinkFor(ds), isLocalCliOpenReady(ds, { cliId: effectiveCliId }));
2176
+ const cardJson = buildStreamingCard(ds.session.sessionId, sessionAnchorId(ds), readUrl, turnTitle, ds.lastScreenContent || '', ds.lastScreenStatus || 'working', effectiveCliId, next, ds.streamCardNonce, ds.currentImageKey, !!ds.adoptedFrom, false, localeForBot(ds.larkAppId), cardUsageLimit(ds), writableTerminalLinkFor(ds), isLocalCliOpenReady(ds, { cliId: effectiveCliId }), getDaemonStreamingCardUsageSnapshot(ds, effectiveCliId));
1983
2177
  updateMessage(ds.larkAppId, cardMessageId, cardJson).catch(err => logger.debug(`[${tag(ds)}] Failed to migrate unknown frozen card: ${err}`));
1984
2178
  logger.info(`[${tag(ds)}] Migrated unknown frozen card to ${next} (legacy nonce=${clickedNonce})`);
1985
2179
  try {
@@ -2000,13 +2194,13 @@ export async function handleCardAction(data, deps, larkAppId) {
2000
2194
  const next = nextMode(cur);
2001
2195
  ds.displayMode = next;
2002
2196
  persistStreamCardState(ds);
2003
- if (ds.worker) {
2004
- ds.worker.send({ type: 'set_display_mode', mode: next });
2197
+ if (ds.worker || isSessionTransferring(ds)) {
2198
+ sendWorkerSessionInput(ds, { type: 'set_display_mode', mode: next });
2005
2199
  }
2006
2200
  const effectiveCliId = sessionCliId(ds);
2007
- const readUrl = ds.workerPort ? buildTerminalUrl(ds) : '';
2201
+ const readUrl = readableTerminalUrlFor(ds);
2008
2202
  const turnTitle = ds.currentTurnTitle || ds.session.title || getCliDisplayName(effectiveCliId);
2009
- const cardJson = buildStreamingCard(ds.session.sessionId, sessionAnchorId(ds), readUrl, turnTitle, ds.lastScreenContent || '', ds.lastScreenStatus || 'working', effectiveCliId, next, ds.streamCardNonce, ds.currentImageKey, !!ds.adoptedFrom, false, localeForBot(ds.larkAppId), cardUsageLimit(ds), writableTerminalLinkFor(ds), isLocalCliOpenReady(ds, { cliId: effectiveCliId }));
2203
+ const cardJson = buildStreamingCard(ds.session.sessionId, sessionAnchorId(ds), readUrl, turnTitle, ds.lastScreenContent || '', ds.lastScreenStatus || 'working', effectiveCliId, next, ds.streamCardNonce, ds.currentImageKey, !!ds.adoptedFrom, false, localeForBot(ds.larkAppId), cardUsageLimit(ds), writableTerminalLinkFor(ds), isLocalCliOpenReady(ds, { cliId: effectiveCliId }), getDaemonStreamingCardUsageSnapshot(ds, effectiveCliId));
2010
2204
  updateMessage(ds.larkAppId, frozen.messageId, cardJson).catch(err => logger.debug(`[${tag(ds)}] Failed to migrate frozen card: ${err}`));
2011
2205
  ds.frozenCards.delete(clickedNonce);
2012
2206
  saveFrozenCards(ds.session.sessionId, ds.frozenCards);
@@ -2024,13 +2218,13 @@ export async function handleCardAction(data, deps, larkAppId) {
2024
2218
  const next = nextMode(cur);
2025
2219
  ds.displayMode = next;
2026
2220
  persistStreamCardState(ds);
2027
- if (ds.worker) {
2028
- ds.worker.send({ type: 'set_display_mode', mode: next });
2221
+ if (ds.worker || isSessionTransferring(ds)) {
2222
+ sendWorkerSessionInput(ds, { type: 'set_display_mode', mode: next });
2029
2223
  }
2030
- if (ds.streamCardId && ds.workerPort) {
2031
- const readUrl = buildTerminalUrl(ds);
2224
+ if (ds.streamCardId && workerHasInitialized(ds)) {
2225
+ const readUrl = readableTerminalUrlFor(ds);
2032
2226
  const turnTitle = ds.currentTurnTitle || ds.session.title || getCliDisplayName(effectiveCliId);
2033
- const cardJson = buildStreamingCard(ds.session.sessionId, sessionAnchorId(ds), readUrl, turnTitle, ds.lastScreenContent || '', ds.lastScreenStatus || 'working', effectiveCliId, next, ds.streamCardNonce, ds.currentImageKey, !!ds.adoptedFrom, false, localeForBot(ds.larkAppId), cardUsageLimit(ds), writableTerminalLinkFor(ds), isLocalCliOpenReady(ds, { cliId: effectiveCliId }));
2227
+ const cardJson = buildStreamingCard(ds.session.sessionId, sessionAnchorId(ds), readUrl, turnTitle, ds.lastScreenContent || '', ds.lastScreenStatus || 'working', effectiveCliId, next, ds.streamCardNonce, ds.currentImageKey, !!ds.adoptedFrom, false, localeForBot(ds.larkAppId), cardUsageLimit(ds), writableTerminalLinkFor(ds), isLocalCliOpenReady(ds, { cliId: effectiveCliId }), getDaemonStreamingCardUsageSnapshot(ds, effectiveCliId));
2034
2228
  if (cardMessageId && cardMessageId !== ds.streamCardId) {
2035
2229
  updateMessage(ds.larkAppId, cardMessageId, cardJson).catch(err => logger.debug(`[${tag(ds)}] Failed to migrate clicked legacy card: ${err}`));
2036
2230
  }
@@ -2069,19 +2263,19 @@ export async function handleCardAction(data, deps, larkAppId) {
2069
2263
  }
2070
2264
  // Manual screenshot refresh — force immediate capture bypassing 10s interval + hash dedup.
2071
2265
  if (actionType === 'refresh_screenshot' && ds) {
2072
- if (ds.worker) {
2073
- ds.worker.send({ type: 'refresh_screen' });
2266
+ if (ds.worker || isSessionTransferring(ds)) {
2267
+ sendWorkerSessionInput(ds, { type: 'refresh_screen' });
2074
2268
  logger.info(`[${tag(ds)}] Manual screenshot refresh`);
2075
2269
  }
2076
2270
  // Return the current card JSON so Feishu doesn't revert the displayed
2077
2271
  // image to the originally-POSTed initial frame while waiting for the
2078
2272
  // fresh screenshot PATCH (~1s).
2079
- if (ds.streamCardId && ds.streamCardId !== CARD_POSTING_SENTINEL && ds.workerPort) {
2273
+ if (ds.streamCardId && ds.streamCardId !== CARD_POSTING_SENTINEL && workerHasInitialized(ds)) {
2080
2274
  const botCfg = getBot(ds.larkAppId).config;
2081
2275
  const effectiveCliId = sessionCliId(ds);
2082
- const readUrl = buildTerminalUrl(ds);
2276
+ const readUrl = readableTerminalUrlFor(ds);
2083
2277
  const turnTitle = ds.currentTurnTitle || ds.session.title || getCliDisplayName(effectiveCliId);
2084
- const cardJson = buildStreamingCard(ds.session.sessionId, sessionAnchorId(ds), readUrl, turnTitle, ds.lastScreenContent || '', ds.lastScreenStatus || 'working', effectiveCliId, ds.displayMode ?? 'screenshot', ds.streamCardNonce, ds.currentImageKey, !!ds.adoptedFrom, false, localeForBot(ds.larkAppId), cardUsageLimit(ds), writableTerminalLinkFor(ds), isLocalCliOpenReady(ds, { cliId: effectiveCliId }));
2278
+ const cardJson = buildStreamingCard(ds.session.sessionId, sessionAnchorId(ds), readUrl, turnTitle, ds.lastScreenContent || '', ds.lastScreenStatus || 'working', effectiveCliId, ds.displayMode ?? 'screenshot', ds.streamCardNonce, ds.currentImageKey, !!ds.adoptedFrom, false, localeForBot(ds.larkAppId), cardUsageLimit(ds), writableTerminalLinkFor(ds), isLocalCliOpenReady(ds, { cliId: effectiveCliId }), getDaemonStreamingCardUsageSnapshot(ds, effectiveCliId));
2085
2279
  if (cardMessageId && cardMessageId !== ds.streamCardId) {
2086
2280
  updateMessage(ds.larkAppId, cardMessageId, cardJson).catch(err => logger.debug(`[${tag(ds)}] Failed to migrate clicked legacy card: ${err}`));
2087
2281
  }
@@ -2094,19 +2288,27 @@ export async function handleCardAction(data, deps, larkAppId) {
2094
2288
  }
2095
2289
  // Quick-action keys (Esc, ^C, Tab, Space, Enter, ←↑↓→, ½ page) — forward to worker.
2096
2290
  if (actionType === 'term_action' && ds) {
2291
+ if (isSessionTransferring(ds)) {
2292
+ return {
2293
+ toast: {
2294
+ type: 'warning',
2295
+ content: t('cmd.session.transfer_in_progress', undefined, localeForBot(ds.larkAppId)),
2296
+ },
2297
+ };
2298
+ }
2097
2299
  const key = value?.key;
2098
2300
  if (!key)
2099
2301
  return;
2100
2302
  if (ds.worker) {
2101
- ds.worker.send({ type: 'term_action', key });
2303
+ sendWorkerSessionInput(ds, { type: 'term_action', key });
2102
2304
  logger.info(`[${tag(ds)}] term_action: ${key}`);
2103
2305
  }
2104
- if (ds.streamCardId && ds.streamCardId !== CARD_POSTING_SENTINEL && ds.workerPort) {
2306
+ if (ds.streamCardId && ds.streamCardId !== CARD_POSTING_SENTINEL && workerHasInitialized(ds)) {
2105
2307
  const botCfg = getBot(ds.larkAppId).config;
2106
2308
  const effectiveCliId = sessionCliId(ds);
2107
- const readUrl = buildTerminalUrl(ds);
2309
+ const readUrl = readableTerminalUrlFor(ds);
2108
2310
  const turnTitle = ds.currentTurnTitle || ds.session.title || getCliDisplayName(effectiveCliId);
2109
- const cardJson = buildStreamingCard(ds.session.sessionId, sessionAnchorId(ds), readUrl, turnTitle, ds.lastScreenContent || '', ds.lastScreenStatus || 'working', effectiveCliId, ds.displayMode ?? 'screenshot', ds.streamCardNonce, ds.currentImageKey, !!ds.adoptedFrom, false, localeForBot(ds.larkAppId), cardUsageLimit(ds), writableTerminalLinkFor(ds), isLocalCliOpenReady(ds, { cliId: effectiveCliId }));
2311
+ const cardJson = buildStreamingCard(ds.session.sessionId, sessionAnchorId(ds), readUrl, turnTitle, ds.lastScreenContent || '', ds.lastScreenStatus || 'working', effectiveCliId, ds.displayMode ?? 'screenshot', ds.streamCardNonce, ds.currentImageKey, !!ds.adoptedFrom, false, localeForBot(ds.larkAppId), cardUsageLimit(ds), writableTerminalLinkFor(ds), isLocalCliOpenReady(ds, { cliId: effectiveCliId }), getDaemonStreamingCardUsageSnapshot(ds, effectiveCliId));
2110
2312
  try {
2111
2313
  return JSON.parse(cardJson);
2112
2314
  }
@@ -2261,6 +2463,10 @@ export async function handleCardAction(data, deps, larkAppId) {
2261
2463
  const ds = activeSessions.get(sKey);
2262
2464
  if (!ds)
2263
2465
  return;
2466
+ const sourceSession = ds.session;
2467
+ if (isSessionTransferring(ds)) {
2468
+ return { toast: { type: 'warning', content: t('cmd.session.transfer_in_progress', undefined, localeForBot(ds.larkAppId)) } };
2469
+ }
2264
2470
  if (!canOperate(ds.larkAppId, ds.chatId, operatorOpenId)) {
2265
2471
  logger.info(`codex_app_thread_select blocked for non-operator user: ${operatorOpenId} (chat=${ds.chatId})`);
2266
2472
  return { toast: { type: 'error', content: t('card.grant.toast_no_repo_perm', undefined, localeForBot(ds.larkAppId)) } };
@@ -2290,6 +2496,12 @@ export async function handleCardAction(data, deps, larkAppId) {
2290
2496
  await sessionReply(rootId, t('cmd.codex_app_adopt.list_failed', { error: err?.message ?? String(err) }, localeForBot(ds.larkAppId)));
2291
2497
  return;
2292
2498
  }
2499
+ if (ds.session !== sourceSession
2500
+ || ds.session.status !== 'active'
2501
+ || activeSessions.get(sKey) !== ds
2502
+ || isSessionTransferring(ds)) {
2503
+ return { toast: { type: 'warning', content: t('cmd.session.transfer_in_progress', undefined, localeForBot(ds.larkAppId)) } };
2504
+ }
2293
2505
  const target = threads.find(t => t.threadId === selected.threadId);
2294
2506
  if (!target) {
2295
2507
  await sessionReply(rootId, t('cmd.codex_app_adopt.thread_not_found', { threadId: selected.threadId }, localeForBot(ds.larkAppId)));
@@ -2311,6 +2523,10 @@ export async function handleCardAction(data, deps, larkAppId) {
2311
2523
  const ds = activeSessions.get(sKey);
2312
2524
  if (!ds)
2313
2525
  return;
2526
+ const sourceSession = ds.session;
2527
+ if (isSessionTransferring(ds)) {
2528
+ return { toast: { type: 'warning', content: t('cmd.session.transfer_in_progress', undefined, localeForBot(ds.larkAppId)) } };
2529
+ }
2314
2530
  // /adopt 是管理动作:下拉入口同样要求 canOperate(命令路径已在 daemon 层 gate)。
2315
2531
  if (!canOperate(ds.larkAppId, ds.chatId, operatorOpenId)) {
2316
2532
  logger.info(`adopt_select blocked for non-operator user: ${operatorOpenId} (chat=${ds.chatId})`);
@@ -2375,6 +2591,12 @@ export async function handleCardAction(data, deps, larkAppId) {
2375
2591
  await new Promise(r => setTimeout(r, 150));
2376
2592
  target = await resolveAdoptTarget();
2377
2593
  }
2594
+ if (ds.session !== sourceSession
2595
+ || ds.session.status !== 'active'
2596
+ || activeSessions.get(sKey) !== ds
2597
+ || isSessionTransferring(ds)) {
2598
+ return { toast: { type: 'warning', content: t('cmd.session.transfer_in_progress', undefined, localeForBot(ds.larkAppId)) } };
2599
+ }
2378
2600
  if (!target) {
2379
2601
  await sessionReply(rootId, t('cmd.adopt.target_exited', undefined, localeForBot(ds.larkAppId)));
2380
2602
  if (cardMessageId && larkAppId)
@@ -2398,6 +2620,10 @@ export async function handleCardAction(data, deps, larkAppId) {
2398
2620
  const ds = activeSessions.get(sKey);
2399
2621
  if (!ds)
2400
2622
  return;
2623
+ const sourceSession = ds.session;
2624
+ if (isSessionTransferring(ds)) {
2625
+ return { toast: { type: 'warning', content: t('cmd.session.transfer_in_progress', undefined, localeForBot(ds.larkAppId)) } };
2626
+ }
2401
2627
  if (!canOperate(ds.larkAppId, ds.chatId, operatorOpenId)) {
2402
2628
  logger.info(`adopt_resume_select blocked for non-operator user: ${operatorOpenId} (chat=${ds.chatId})`);
2403
2629
  return { toast: { type: 'error', content: t('card.grant.toast_no_repo_perm', undefined, localeForBot(ds.larkAppId)) } };
@@ -2416,6 +2642,12 @@ export async function handleCardAction(data, deps, larkAppId) {
2416
2642
  const botCfg = getBot(ds.larkAppId).config;
2417
2643
  const { discoverResumableSessionsForBot, startResumeImportSession } = await import('../../core/command-handler.js');
2418
2644
  const resumable = await discoverResumableSessionsForBot(botCfg.cliId, botCfg.cliPathOverride, activeSessions);
2645
+ if (ds.session !== sourceSession
2646
+ || ds.session.status !== 'active'
2647
+ || activeSessions.get(sKey) !== ds
2648
+ || isSessionTransferring(ds)) {
2649
+ return { toast: { type: 'warning', content: t('cmd.session.transfer_in_progress', undefined, localeForBot(ds.larkAppId)) } };
2650
+ }
2419
2651
  const target = resumable.find(r => r.cliSessionId === selected.cliSessionId);
2420
2652
  if (!target) {
2421
2653
  await sessionReply(rootId, t('cmd.adopt.resume_not_found', { id: selected.cliSessionId }, localeForBot(ds.larkAppId)));