botmux 3.10.0 → 3.11.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 (539) hide show
  1. package/dist/.runtime-build-id +1 -1
  2. package/dist/adapters/backend/herdr-backend.d.ts +4 -3
  3. package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
  4. package/dist/adapters/backend/herdr-backend.js +11 -8
  5. package/dist/adapters/backend/herdr-backend.js.map +1 -1
  6. package/dist/adapters/backend/pty-backend.d.ts +1 -1
  7. package/dist/adapters/backend/pty-backend.d.ts.map +1 -1
  8. package/dist/adapters/backend/pty-backend.js +4 -1
  9. package/dist/adapters/backend/pty-backend.js.map +1 -1
  10. package/dist/adapters/backend/riff-backend.d.ts +29 -5
  11. package/dist/adapters/backend/riff-backend.d.ts.map +1 -1
  12. package/dist/adapters/backend/riff-backend.js +302 -31
  13. package/dist/adapters/backend/riff-backend.js.map +1 -1
  14. package/dist/adapters/backend/sandbox.d.ts +6 -0
  15. package/dist/adapters/backend/sandbox.d.ts.map +1 -1
  16. package/dist/adapters/backend/sandbox.js +7 -0
  17. package/dist/adapters/backend/sandbox.js.map +1 -1
  18. package/dist/adapters/backend/tmux-backend.d.ts +1 -1
  19. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  20. package/dist/adapters/backend/tmux-backend.js +4 -1
  21. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  22. package/dist/adapters/backend/tmux-pipe-backend.d.ts +1 -1
  23. package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
  24. package/dist/adapters/backend/tmux-pipe-backend.js +1 -1
  25. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
  26. package/dist/adapters/backend/types.d.ts +37 -3
  27. package/dist/adapters/backend/types.d.ts.map +1 -1
  28. package/dist/adapters/backend/types.js.map +1 -1
  29. package/dist/adapters/backend/zellij-backend.d.ts +3 -3
  30. package/dist/adapters/backend/zellij-backend.d.ts.map +1 -1
  31. package/dist/adapters/backend/zellij-backend.js +9 -3
  32. package/dist/adapters/backend/zellij-backend.js.map +1 -1
  33. package/dist/adapters/backend/zellij-observe-backend.d.ts +3 -3
  34. package/dist/adapters/backend/zellij-observe-backend.d.ts.map +1 -1
  35. package/dist/adapters/backend/zellij-observe-backend.js +12 -7
  36. package/dist/adapters/backend/zellij-observe-backend.js.map +1 -1
  37. package/dist/adapters/cli/codex-app.d.ts.map +1 -1
  38. package/dist/adapters/cli/codex-app.js +11 -2
  39. package/dist/adapters/cli/codex-app.js.map +1 -1
  40. package/dist/adapters/cli/read-isolation.d.ts +35 -2
  41. package/dist/adapters/cli/read-isolation.d.ts.map +1 -1
  42. package/dist/adapters/cli/read-isolation.js +88 -7
  43. package/dist/adapters/cli/read-isolation.js.map +1 -1
  44. package/dist/adapters/cli/runner-input.d.ts +15 -14
  45. package/dist/adapters/cli/runner-input.d.ts.map +1 -1
  46. package/dist/adapters/cli/runner-input.js +64 -26
  47. package/dist/adapters/cli/runner-input.js.map +1 -1
  48. package/dist/adapters/cli/types.d.ts +20 -5
  49. package/dist/adapters/cli/types.d.ts.map +1 -1
  50. package/dist/bot-registry.d.ts +24 -0
  51. package/dist/bot-registry.d.ts.map +1 -1
  52. package/dist/bot-registry.js +110 -5
  53. package/dist/bot-registry.js.map +1 -1
  54. package/dist/cli/fleet-shutdown.d.ts +47 -0
  55. package/dist/cli/fleet-shutdown.d.ts.map +1 -0
  56. package/dist/cli/fleet-shutdown.js +387 -0
  57. package/dist/cli/fleet-shutdown.js.map +1 -0
  58. package/dist/cli/pm2-descriptor-guard.d.ts +19 -0
  59. package/dist/cli/pm2-descriptor-guard.d.ts.map +1 -0
  60. package/dist/cli/pm2-descriptor-guard.js +122 -0
  61. package/dist/cli/pm2-descriptor-guard.js.map +1 -0
  62. package/dist/cli/pm2-exact-start.d.ts +16 -0
  63. package/dist/cli/pm2-exact-start.d.ts.map +1 -0
  64. package/dist/cli/pm2-exact-start.js +63 -0
  65. package/dist/cli/pm2-exact-start.js.map +1 -0
  66. package/dist/cli/pm2-god-admission.d.ts +8 -0
  67. package/dist/cli/pm2-god-admission.d.ts.map +1 -0
  68. package/dist/cli/pm2-god-admission.js +25 -0
  69. package/dist/cli/pm2-god-admission.js.map +1 -0
  70. package/dist/cli/pm2-jlist.d.ts +13 -0
  71. package/dist/cli/pm2-jlist.d.ts.map +1 -0
  72. package/dist/cli/pm2-jlist.js +108 -0
  73. package/dist/cli/pm2-jlist.js.map +1 -0
  74. package/dist/cli/pm2-preflight.d.ts +10 -0
  75. package/dist/cli/pm2-preflight.d.ts.map +1 -0
  76. package/dist/cli/pm2-preflight.js +23 -0
  77. package/dist/cli/pm2-preflight.js.map +1 -0
  78. package/dist/cli/pm2-shutdown-capability.d.ts +27 -0
  79. package/dist/cli/pm2-shutdown-capability.d.ts.map +1 -0
  80. package/dist/cli/pm2-shutdown-capability.js +161 -0
  81. package/dist/cli/pm2-shutdown-capability.js.map +1 -0
  82. package/dist/cli/pm2-start-transaction.d.ts +55 -0
  83. package/dist/cli/pm2-start-transaction.d.ts.map +1 -0
  84. package/dist/cli/pm2-start-transaction.js +220 -0
  85. package/dist/cli/pm2-start-transaction.js.map +1 -0
  86. package/dist/cli/send-dispatch.d.ts +12 -0
  87. package/dist/cli/send-dispatch.d.ts.map +1 -1
  88. package/dist/cli/send-dispatch.js +31 -4
  89. package/dist/cli/send-dispatch.js.map +1 -1
  90. package/dist/cli/session-list-liveness.d.ts +1 -0
  91. package/dist/cli/session-list-liveness.d.ts.map +1 -1
  92. package/dist/cli/session-list-liveness.js +6 -0
  93. package/dist/cli/session-list-liveness.js.map +1 -1
  94. package/dist/cli/session-picker-layout.d.ts +26 -0
  95. package/dist/cli/session-picker-layout.d.ts.map +1 -0
  96. package/dist/cli/session-picker-layout.js +123 -0
  97. package/dist/cli/session-picker-layout.js.map +1 -0
  98. package/dist/cli/session-picker-viewport.d.ts +49 -0
  99. package/dist/cli/session-picker-viewport.d.ts.map +1 -0
  100. package/dist/cli/session-picker-viewport.js +46 -0
  101. package/dist/cli/session-picker-viewport.js.map +1 -0
  102. package/dist/cli/supervisor-shutdown-client.d.ts +31 -0
  103. package/dist/cli/supervisor-shutdown-client.d.ts.map +1 -0
  104. package/dist/cli/supervisor-shutdown-client.js +146 -0
  105. package/dist/cli/supervisor-shutdown-client.js.map +1 -0
  106. package/dist/cli/terminal-width.d.ts +48 -0
  107. package/dist/cli/terminal-width.d.ts.map +1 -0
  108. package/dist/cli/terminal-width.js +119 -0
  109. package/dist/cli/terminal-width.js.map +1 -0
  110. package/dist/cli/vc-agent.d.ts.map +1 -1
  111. package/dist/cli/vc-agent.js +1 -1
  112. package/dist/cli/vc-agent.js.map +1 -1
  113. package/dist/cli.d.ts.map +1 -1
  114. package/dist/cli.js +2269 -716
  115. package/dist/cli.js.map +1 -1
  116. package/dist/codex-app-runner.js +1630 -125
  117. package/dist/codex-app-runner.js.map +1 -1
  118. package/dist/core/ask-api.d.ts +8 -1
  119. package/dist/core/ask-api.d.ts.map +1 -1
  120. package/dist/core/ask-api.js +18 -0
  121. package/dist/core/ask-api.js.map +1 -1
  122. package/dist/core/ask-broker.d.ts +20 -0
  123. package/dist/core/ask-broker.d.ts.map +1 -1
  124. package/dist/core/ask-broker.js +467 -47
  125. package/dist/core/ask-broker.js.map +1 -1
  126. package/dist/core/ask-persist-store.d.ts +126 -0
  127. package/dist/core/ask-persist-store.d.ts.map +1 -0
  128. package/dist/core/ask-persist-store.js +188 -0
  129. package/dist/core/ask-persist-store.js.map +1 -0
  130. package/dist/core/ask-types.d.ts +80 -0
  131. package/dist/core/ask-types.d.ts.map +1 -1
  132. package/dist/core/ask-types.js +62 -0
  133. package/dist/core/ask-types.js.map +1 -1
  134. package/dist/core/cli-session-id-publisher.d.ts +20 -0
  135. package/dist/core/cli-session-id-publisher.d.ts.map +1 -0
  136. package/dist/core/cli-session-id-publisher.js +21 -0
  137. package/dist/core/cli-session-id-publisher.js.map +1 -0
  138. package/dist/core/command-handler.d.ts.map +1 -1
  139. package/dist/core/command-handler.js +427 -255
  140. package/dist/core/command-handler.js.map +1 -1
  141. package/dist/core/cost-calculator.d.ts +13 -0
  142. package/dist/core/cost-calculator.d.ts.map +1 -1
  143. package/dist/core/cost-calculator.js +385 -26
  144. package/dist/core/cost-calculator.js.map +1 -1
  145. package/dist/core/dashboard-ipc-server.d.ts +9 -0
  146. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  147. package/dist/core/dashboard-ipc-server.js +842 -288
  148. package/dist/core/dashboard-ipc-server.js.map +1 -1
  149. package/dist/core/explicit-session-backing-cleanup.d.ts +49 -0
  150. package/dist/core/explicit-session-backing-cleanup.d.ts.map +1 -0
  151. package/dist/core/explicit-session-backing-cleanup.js +76 -0
  152. package/dist/core/explicit-session-backing-cleanup.js.map +1 -0
  153. package/dist/core/idle-worker-sweeper.d.ts +20 -0
  154. package/dist/core/idle-worker-sweeper.d.ts.map +1 -1
  155. package/dist/core/idle-worker-sweeper.js +18 -0
  156. package/dist/core/idle-worker-sweeper.js.map +1 -1
  157. package/dist/core/inflight-input-tracker.d.ts +10 -0
  158. package/dist/core/inflight-input-tracker.d.ts.map +1 -1
  159. package/dist/core/inflight-input-tracker.js +7 -0
  160. package/dist/core/inflight-input-tracker.js.map +1 -1
  161. package/dist/core/managed-origin-attestation.d.ts +51 -0
  162. package/dist/core/managed-origin-attestation.d.ts.map +1 -0
  163. package/dist/core/managed-origin-attestation.js +208 -0
  164. package/dist/core/managed-origin-attestation.js.map +1 -0
  165. package/dist/core/managed-origin-capability.d.ts +62 -3
  166. package/dist/core/managed-origin-capability.d.ts.map +1 -1
  167. package/dist/core/managed-origin-capability.js +419 -9
  168. package/dist/core/managed-origin-capability.js.map +1 -1
  169. package/dist/core/pending-repo-journal.d.ts +6 -0
  170. package/dist/core/pending-repo-journal.d.ts.map +1 -0
  171. package/dist/core/pending-repo-journal.js +85 -0
  172. package/dist/core/pending-repo-journal.js.map +1 -0
  173. package/dist/core/persistent-backend.d.ts +6 -1
  174. package/dist/core/persistent-backend.d.ts.map +1 -1
  175. package/dist/core/persistent-backend.js +12 -6
  176. package/dist/core/persistent-backend.js.map +1 -1
  177. package/dist/core/process-start-identity.d.ts +3 -0
  178. package/dist/core/process-start-identity.d.ts.map +1 -0
  179. package/dist/core/process-start-identity.js +62 -0
  180. package/dist/core/process-start-identity.js.map +1 -0
  181. package/dist/core/raw-command-writer.d.ts +34 -0
  182. package/dist/core/raw-command-writer.d.ts.map +1 -0
  183. package/dist/core/raw-command-writer.js +48 -0
  184. package/dist/core/raw-command-writer.js.map +1 -0
  185. package/dist/core/reply-target.d.ts +85 -22
  186. package/dist/core/reply-target.d.ts.map +1 -1
  187. package/dist/core/reply-target.js +267 -32
  188. package/dist/core/reply-target.js.map +1 -1
  189. package/dist/core/resource-monitor/runtime.d.ts.map +1 -1
  190. package/dist/core/resource-monitor/runtime.js +4 -1
  191. package/dist/core/resource-monitor/runtime.js.map +1 -1
  192. package/dist/core/restart-report.d.ts +6 -1
  193. package/dist/core/restart-report.d.ts.map +1 -1
  194. package/dist/core/restart-report.js +22 -4
  195. package/dist/core/restart-report.js.map +1 -1
  196. package/dist/core/riff-shutdown-detach.d.ts +164 -0
  197. package/dist/core/riff-shutdown-detach.d.ts.map +1 -0
  198. package/dist/core/riff-shutdown-detach.js +787 -0
  199. package/dist/core/riff-shutdown-detach.js.map +1 -0
  200. package/dist/core/riff-worker-shutdown-readiness.d.ts +17 -0
  201. package/dist/core/riff-worker-shutdown-readiness.d.ts.map +1 -0
  202. package/dist/core/riff-worker-shutdown-readiness.js +21 -0
  203. package/dist/core/riff-worker-shutdown-readiness.js.map +1 -0
  204. package/dist/core/session-manager.d.ts +6 -6
  205. package/dist/core/session-manager.d.ts.map +1 -1
  206. package/dist/core/session-manager.js +1227 -594
  207. package/dist/core/session-manager.js.map +1 -1
  208. package/dist/core/session-marker.d.ts +1 -1
  209. package/dist/core/session-marker.d.ts.map +1 -1
  210. package/dist/core/session-marker.js +2 -2
  211. package/dist/core/session-marker.js.map +1 -1
  212. package/dist/core/session-mutation-guard.d.ts +17 -0
  213. package/dist/core/session-mutation-guard.d.ts.map +1 -0
  214. package/dist/core/session-mutation-guard.js +33 -0
  215. package/dist/core/session-mutation-guard.js.map +1 -0
  216. package/dist/core/shutdown-budgets.d.ts +27 -0
  217. package/dist/core/shutdown-budgets.d.ts.map +1 -0
  218. package/dist/core/shutdown-budgets.js +47 -0
  219. package/dist/core/shutdown-budgets.js.map +1 -0
  220. package/dist/core/supervisor-shutdown-ipc.d.ts +10 -0
  221. package/dist/core/supervisor-shutdown-ipc.d.ts.map +1 -0
  222. package/dist/core/supervisor-shutdown-ipc.js +10 -0
  223. package/dist/core/supervisor-shutdown-ipc.js.map +1 -0
  224. package/dist/core/supervisor-shutdown-protocol.d.ts +16 -0
  225. package/dist/core/supervisor-shutdown-protocol.d.ts.map +1 -0
  226. package/dist/core/supervisor-shutdown-protocol.js +16 -0
  227. package/dist/core/supervisor-shutdown-protocol.js.map +1 -0
  228. package/dist/core/trigger-final-suppression.d.ts +4 -0
  229. package/dist/core/trigger-final-suppression.d.ts.map +1 -1
  230. package/dist/core/trigger-final-suppression.js +6 -11
  231. package/dist/core/trigger-final-suppression.js.map +1 -1
  232. package/dist/core/trigger-session.d.ts +70 -0
  233. package/dist/core/trigger-session.d.ts.map +1 -1
  234. package/dist/core/trigger-session.js +923 -145
  235. package/dist/core/trigger-session.js.map +1 -1
  236. package/dist/core/types.d.ts +94 -1
  237. package/dist/core/types.d.ts.map +1 -1
  238. package/dist/core/types.js +9 -0
  239. package/dist/core/types.js.map +1 -1
  240. package/dist/core/worker-pool.d.ts +194 -13
  241. package/dist/core/worker-pool.d.ts.map +1 -1
  242. package/dist/core/worker-pool.js +2597 -547
  243. package/dist/core/worker-pool.js.map +1 -1
  244. package/dist/daemon.d.ts +76 -2
  245. package/dist/daemon.d.ts.map +1 -1
  246. package/dist/daemon.js +2255 -773
  247. package/dist/daemon.js.map +1 -1
  248. package/dist/dashboard/bot-payload.d.ts +20 -0
  249. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  250. package/dist/dashboard/bot-payload.js +24 -0
  251. package/dist/dashboard/bot-payload.js.map +1 -1
  252. package/dist/dashboard/feed-groups.d.ts +17 -0
  253. package/dist/dashboard/feed-groups.d.ts.map +1 -0
  254. package/dist/dashboard/feed-groups.js +75 -0
  255. package/dist/dashboard/feed-groups.js.map +1 -0
  256. package/dist/dashboard/session-card-model.d.ts +1 -1
  257. package/dist/dashboard/session-card-model.d.ts.map +1 -1
  258. package/dist/dashboard/session-card-model.js +8 -5
  259. package/dist/dashboard/session-card-model.js.map +1 -1
  260. package/dist/dashboard/web/bot-defaults-page.d.ts +4 -0
  261. package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
  262. package/dist/dashboard/web/bot-defaults-page.js +139 -46
  263. package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
  264. package/dist/dashboard/web/bot-defaults.d.ts +4 -0
  265. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  266. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  267. package/dist/dashboard/web/connectors-page.d.ts.map +1 -1
  268. package/dist/dashboard/web/connectors-page.js +40 -3
  269. package/dist/dashboard/web/connectors-page.js.map +1 -1
  270. package/dist/dashboard/web/groups-page.d.ts.map +1 -1
  271. package/dist/dashboard/web/groups-page.js +95 -2
  272. package/dist/dashboard/web/groups-page.js.map +1 -1
  273. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  274. package/dist/dashboard/web/i18n.js +86 -40
  275. package/dist/dashboard/web/i18n.js.map +1 -1
  276. package/dist/dashboard/web/kanban-model.js +1 -1
  277. package/dist/dashboard/web/kanban-model.js.map +1 -1
  278. package/dist/dashboard/web/overview-page.d.ts.map +1 -1
  279. package/dist/dashboard/web/overview-page.js +3 -2
  280. package/dist/dashboard/web/overview-page.js.map +1 -1
  281. package/dist/dashboard/web/overview.d.ts +3 -0
  282. package/dist/dashboard/web/overview.d.ts.map +1 -1
  283. package/dist/dashboard/web/overview.js +2 -0
  284. package/dist/dashboard/web/overview.js.map +1 -1
  285. package/dist/dashboard/web/sessions-kanban.d.ts.map +1 -1
  286. package/dist/dashboard/web/sessions-kanban.js +2 -0
  287. package/dist/dashboard/web/sessions-kanban.js.map +1 -1
  288. package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
  289. package/dist/dashboard/web/sessions-page.js +103 -2
  290. package/dist/dashboard/web/sessions-page.js.map +1 -1
  291. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  292. package/dist/dashboard/web/sessions.js +3 -2
  293. package/dist/dashboard/web/sessions.js.map +1 -1
  294. package/dist/dashboard/web/ui.d.ts +8 -0
  295. package/dist/dashboard/web/ui.d.ts.map +1 -1
  296. package/dist/dashboard/web/ui.js +15 -0
  297. package/dist/dashboard/web/ui.js.map +1 -1
  298. package/dist/dashboard-web/app.js +1 -1
  299. package/dist/dashboard-web/chunks/bot-defaults-page-GED4FEMS.js +10 -0
  300. package/dist/dashboard-web/chunks/chunk-6J5LS5AU.js +51 -0
  301. package/dist/dashboard-web/chunks/{chunk-ZU3UUKAR.js → chunk-HK7NIBJG.js} +1 -1
  302. package/dist/dashboard-web/chunks/{chunk-3UE7S77S.js → chunk-KRHWZEXU.js} +1 -1
  303. package/dist/dashboard-web/chunks/chunk-VBXJOJFH.js +1 -0
  304. package/dist/dashboard-web/chunks/connectors-page-EELM66G3.js +1 -0
  305. package/dist/dashboard-web/chunks/groups-page-WKUV6E42.js +8 -0
  306. package/dist/dashboard-web/chunks/{insights-page-T2ZX2RWL.js → insights-page-54UBPPOC.js} +1 -1
  307. package/dist/dashboard-web/chunks/{monitor-room-YNVBAGR6.js → monitor-room-PORHVKRH.js} +1 -1
  308. package/dist/dashboard-web/chunks/{monitoring-page-LHM55YWN.js → monitoring-page-XGA7AXNL.js} +1 -1
  309. package/dist/dashboard-web/chunks/{office-page-ATXVP7PW.js → office-page-AO3AFG45.js} +1 -1
  310. package/dist/dashboard-web/chunks/overview-page-F5BUTYR6.js +1 -0
  311. package/dist/dashboard-web/chunks/{roles-page-56VWM6CD.js → roles-page-M5GIASJU.js} +1 -1
  312. package/dist/dashboard-web/chunks/{schedules-page-S5K6U4EO.js → schedules-page-O232WQRR.js} +1 -1
  313. package/dist/dashboard-web/chunks/sessions-page-YFRPSTIL.js +6 -0
  314. package/dist/dashboard-web/chunks/{settings-page-QUZDQARR.js → settings-page-OJAZLZQT.js} +1 -1
  315. package/dist/dashboard-web/chunks/{skills-page-IFY7AEND.js → skills-page-EFZQPWV4.js} +1 -1
  316. package/dist/dashboard-web/chunks/{team-federation-page-C7FJQPFZ.js → team-federation-page-ZJVAP7HW.js} +1 -1
  317. package/dist/dashboard-web/chunks/{v3-page-2LMF3WWR.js → v3-page-GYWTRGO2.js} +1 -1
  318. package/dist/dashboard-web/chunks/{whiteboards-page-3XC6QRMY.js → whiteboards-page-MSDAG6P7.js} +1 -1
  319. package/dist/dashboard-web/style.css +255 -0
  320. package/dist/dashboard.js +172 -7
  321. package/dist/dashboard.js.map +1 -1
  322. package/dist/desktop/renderer/app.js +21 -0
  323. package/dist/desktop/renderer/app.js.map +1 -1
  324. package/dist/i18n/en.d.ts.map +1 -1
  325. package/dist/i18n/en.js +35 -9
  326. package/dist/i18n/en.js.map +1 -1
  327. package/dist/i18n/zh.d.ts.map +1 -1
  328. package/dist/i18n/zh.js +35 -9
  329. package/dist/i18n/zh.js.map +1 -1
  330. package/dist/im/lark/ask-card.d.ts +23 -0
  331. package/dist/im/lark/ask-card.d.ts.map +1 -1
  332. package/dist/im/lark/ask-card.js +94 -4
  333. package/dist/im/lark/ask-card.js.map +1 -1
  334. package/dist/im/lark/card-builder.d.ts +6 -1
  335. package/dist/im/lark/card-builder.d.ts.map +1 -1
  336. package/dist/im/lark/card-builder.js +112 -15
  337. package/dist/im/lark/card-builder.js.map +1 -1
  338. package/dist/im/lark/card-handler.d.ts +1 -1
  339. package/dist/im/lark/card-handler.d.ts.map +1 -1
  340. package/dist/im/lark/card-handler.js +496 -310
  341. package/dist/im/lark/card-handler.js.map +1 -1
  342. package/dist/im/lark/client.d.ts +7 -0
  343. package/dist/im/lark/client.d.ts.map +1 -1
  344. package/dist/im/lark/client.js +39 -26
  345. package/dist/im/lark/client.js.map +1 -1
  346. package/dist/im/lark/doc-comment.d.ts +7 -4
  347. package/dist/im/lark/doc-comment.d.ts.map +1 -1
  348. package/dist/im/lark/doc-comment.js +19 -6
  349. package/dist/im/lark/doc-comment.js.map +1 -1
  350. package/dist/im/lark/event-dispatcher.d.ts +16 -0
  351. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  352. package/dist/im/lark/event-dispatcher.js +88 -23
  353. package/dist/im/lark/event-dispatcher.js.map +1 -1
  354. package/dist/im/lark/forward-followup-content.d.ts.map +1 -1
  355. package/dist/im/lark/forward-followup-content.js +9 -1
  356. package/dist/im/lark/forward-followup-content.js.map +1 -1
  357. package/dist/im/lark/grant-command.d.ts.map +1 -1
  358. package/dist/im/lark/grant-command.js +7 -4
  359. package/dist/im/lark/grant-command.js.map +1 -1
  360. package/dist/im/lark/md-card.d.ts +11 -0
  361. package/dist/im/lark/md-card.d.ts.map +1 -1
  362. package/dist/im/lark/md-card.js +46 -0
  363. package/dist/im/lark/md-card.js.map +1 -1
  364. package/dist/im/lark/message-parser.d.ts +23 -2
  365. package/dist/im/lark/message-parser.d.ts.map +1 -1
  366. package/dist/im/lark/message-parser.js +58 -3
  367. package/dist/im/lark/message-parser.js.map +1 -1
  368. package/dist/im/lark/overview-card.d.ts.map +1 -1
  369. package/dist/im/lark/overview-card.js +2 -1
  370. package/dist/im/lark/overview-card.js.map +1 -1
  371. package/dist/services/async-trigger-state.d.ts +6 -1
  372. package/dist/services/async-trigger-state.d.ts.map +1 -1
  373. package/dist/services/async-trigger-state.js +21 -0
  374. package/dist/services/async-trigger-state.js.map +1 -1
  375. package/dist/services/async-trigger-store.d.ts +23 -1
  376. package/dist/services/async-trigger-store.d.ts.map +1 -1
  377. package/dist/services/async-trigger-store.js +96 -13
  378. package/dist/services/async-trigger-store.js.map +1 -1
  379. package/dist/services/bridge-fallback-gate.d.ts +40 -0
  380. package/dist/services/bridge-fallback-gate.d.ts.map +1 -1
  381. package/dist/services/bridge-fallback-gate.js +124 -24
  382. package/dist/services/bridge-fallback-gate.js.map +1 -1
  383. package/dist/services/codex-app-runner-protocol.d.ts +8 -0
  384. package/dist/services/codex-app-runner-protocol.d.ts.map +1 -1
  385. package/dist/services/codex-app-runner-protocol.js +9 -2
  386. package/dist/services/codex-app-runner-protocol.js.map +1 -1
  387. package/dist/services/codex-bridge-queue.d.ts +1 -0
  388. package/dist/services/codex-bridge-queue.d.ts.map +1 -1
  389. package/dist/services/codex-bridge-queue.js +1 -0
  390. package/dist/services/codex-bridge-queue.js.map +1 -1
  391. package/dist/services/codex-service-tier.d.ts +2 -0
  392. package/dist/services/codex-service-tier.d.ts.map +1 -1
  393. package/dist/services/codex-service-tier.js +1 -0
  394. package/dist/services/codex-service-tier.js.map +1 -1
  395. package/dist/services/codex-transcript.d.ts +27 -0
  396. package/dist/services/codex-transcript.d.ts.map +1 -1
  397. package/dist/services/codex-transcript.js +340 -2
  398. package/dist/services/codex-transcript.js.map +1 -1
  399. package/dist/services/grant-policy.d.ts +3 -0
  400. package/dist/services/grant-policy.d.ts.map +1 -1
  401. package/dist/services/grant-policy.js +5 -0
  402. package/dist/services/grant-policy.js.map +1 -1
  403. package/dist/services/grant-prefs-store.d.ts +6 -3
  404. package/dist/services/grant-prefs-store.d.ts.map +1 -1
  405. package/dist/services/grant-prefs-store.js +42 -12
  406. package/dist/services/grant-prefs-store.js.map +1 -1
  407. package/dist/services/hook-runner.d.ts +20 -1
  408. package/dist/services/hook-runner.d.ts.map +1 -1
  409. package/dist/services/hook-runner.js +17 -13
  410. package/dist/services/hook-runner.js.map +1 -1
  411. package/dist/services/idempotency-store.d.ts +134 -0
  412. package/dist/services/idempotency-store.d.ts.map +1 -0
  413. package/dist/services/idempotency-store.js +299 -0
  414. package/dist/services/idempotency-store.js.map +1 -0
  415. package/dist/services/jsonl-cursor.d.ts +7 -3
  416. package/dist/services/jsonl-cursor.d.ts.map +1 -1
  417. package/dist/services/jsonl-cursor.js +55 -31
  418. package/dist/services/jsonl-cursor.js.map +1 -1
  419. package/dist/services/open-platform-rename.d.ts.map +1 -1
  420. package/dist/services/open-platform-rename.js +1 -108
  421. package/dist/services/open-platform-rename.js.map +1 -1
  422. package/dist/services/restart-intent-store.d.ts +22 -2
  423. package/dist/services/restart-intent-store.d.ts.map +1 -1
  424. package/dist/services/restart-intent-store.js +160 -18
  425. package/dist/services/restart-intent-store.js.map +1 -1
  426. package/dist/services/sandbox-store.d.ts +22 -0
  427. package/dist/services/sandbox-store.d.ts.map +1 -1
  428. package/dist/services/sandbox-store.js +37 -8
  429. package/dist/services/sandbox-store.js.map +1 -1
  430. package/dist/services/send-policy.d.ts +41 -33
  431. package/dist/services/send-policy.d.ts.map +1 -1
  432. package/dist/services/send-policy.js +39 -40
  433. package/dist/services/send-policy.js.map +1 -1
  434. package/dist/services/session-store.d.ts +66 -0
  435. package/dist/services/session-store.d.ts.map +1 -1
  436. package/dist/services/session-store.js +403 -67
  437. package/dist/services/session-store.js.map +1 -1
  438. package/dist/services/submit-notification.d.ts +3 -0
  439. package/dist/services/submit-notification.d.ts.map +1 -0
  440. package/dist/services/submit-notification.js +15 -0
  441. package/dist/services/submit-notification.js.map +1 -0
  442. package/dist/services/traex-transcript.d.ts +25 -2
  443. package/dist/services/traex-transcript.d.ts.map +1 -1
  444. package/dist/services/traex-transcript.js +133 -1
  445. package/dist/services/traex-transcript.js.map +1 -1
  446. package/dist/services/trigger-types.d.ts +13 -1
  447. package/dist/services/trigger-types.d.ts.map +1 -1
  448. package/dist/services/trigger-types.js +39 -0
  449. package/dist/services/trigger-types.js.map +1 -1
  450. package/dist/services/voice/index.d.ts +3 -2
  451. package/dist/services/voice/index.d.ts.map +1 -1
  452. package/dist/services/voice/index.js +5 -5
  453. package/dist/services/voice/index.js.map +1 -1
  454. package/dist/services/voice/openai.d.ts +2 -1
  455. package/dist/services/voice/openai.d.ts.map +1 -1
  456. package/dist/services/voice/openai.js +2 -1
  457. package/dist/services/voice/openai.js.map +1 -1
  458. package/dist/services/voice/sami.d.ts +5 -2
  459. package/dist/services/voice/sami.d.ts.map +1 -1
  460. package/dist/services/voice/sami.js +19 -4
  461. package/dist/services/voice/sami.js.map +1 -1
  462. package/dist/setup/bots-store.d.ts.map +1 -1
  463. package/dist/setup/bots-store.js +10 -4
  464. package/dist/setup/bots-store.js.map +1 -1
  465. package/dist/setup/lark-scopes.json +2 -0
  466. package/dist/setup/open-platform-automation.d.ts +9 -1
  467. package/dist/setup/open-platform-automation.d.ts.map +1 -1
  468. package/dist/setup/open-platform-automation.js +44 -15
  469. package/dist/setup/open-platform-automation.js.map +1 -1
  470. package/dist/setup/open-platform-visibility.d.ts +42 -0
  471. package/dist/setup/open-platform-visibility.d.ts.map +1 -0
  472. package/dist/setup/open-platform-visibility.js +133 -0
  473. package/dist/setup/open-platform-visibility.js.map +1 -0
  474. package/dist/setup/verify-permissions.d.ts.map +1 -1
  475. package/dist/setup/verify-permissions.js +6 -0
  476. package/dist/setup/verify-permissions.js.map +1 -1
  477. package/dist/skills/definitions.js +2 -2
  478. package/dist/types.d.ts +290 -13
  479. package/dist/types.d.ts.map +1 -1
  480. package/dist/utils/bot-routing.d.ts +1 -0
  481. package/dist/utils/bot-routing.d.ts.map +1 -1
  482. package/dist/utils/bot-routing.js +1 -1
  483. package/dist/utils/bot-routing.js.map +1 -1
  484. package/dist/utils/child-env.d.ts +1 -1
  485. package/dist/utils/child-env.d.ts.map +1 -1
  486. package/dist/utils/child-env.js +9 -0
  487. package/dist/utils/child-env.js.map +1 -1
  488. package/dist/utils/codex-app-control.d.ts +438 -0
  489. package/dist/utils/codex-app-control.d.ts.map +1 -0
  490. package/dist/utils/codex-app-control.js +2336 -0
  491. package/dist/utils/codex-app-control.js.map +1 -0
  492. package/dist/utils/codex-app-dispatch-ledger.d.ts +78 -0
  493. package/dist/utils/codex-app-dispatch-ledger.d.ts.map +1 -0
  494. package/dist/utils/codex-app-dispatch-ledger.js +149 -0
  495. package/dist/utils/codex-app-dispatch-ledger.js.map +1 -0
  496. package/dist/utils/codex-app-turn-dispatch.d.ts +77 -0
  497. package/dist/utils/codex-app-turn-dispatch.d.ts.map +1 -0
  498. package/dist/utils/codex-app-turn-dispatch.js +141 -0
  499. package/dist/utils/codex-app-turn-dispatch.js.map +1 -0
  500. package/dist/utils/codex-app-turn-liveness.d.ts +125 -0
  501. package/dist/utils/codex-app-turn-liveness.d.ts.map +1 -0
  502. package/dist/utils/codex-app-turn-liveness.js +301 -0
  503. package/dist/utils/codex-app-turn-liveness.js.map +1 -0
  504. package/dist/utils/idle-detector.d.ts +12 -0
  505. package/dist/utils/idle-detector.d.ts.map +1 -1
  506. package/dist/utils/idle-detector.js +19 -0
  507. package/dist/utils/idle-detector.js.map +1 -1
  508. package/dist/utils/input-gate.d.ts +87 -0
  509. package/dist/utils/input-gate.d.ts.map +1 -1
  510. package/dist/utils/input-gate.js +86 -0
  511. package/dist/utils/input-gate.js.map +1 -1
  512. package/dist/utils/lark-upload.d.ts.map +1 -1
  513. package/dist/utils/lark-upload.js +52 -1
  514. package/dist/utils/lark-upload.js.map +1 -1
  515. package/dist/utils/pending-input-queue.d.ts +15 -0
  516. package/dist/utils/pending-input-queue.d.ts.map +1 -1
  517. package/dist/utils/pending-input-queue.js +5 -1
  518. package/dist/utils/pending-input-queue.js.map +1 -1
  519. package/dist/utils/user-token.d.ts.map +1 -1
  520. package/dist/utils/user-token.js +53 -2
  521. package/dist/utils/user-token.js.map +1 -1
  522. package/dist/worker-ipc-preload.d.ts +12 -0
  523. package/dist/worker-ipc-preload.d.ts.map +1 -0
  524. package/dist/worker-ipc-preload.js +44 -0
  525. package/dist/worker-ipc-preload.js.map +1 -0
  526. package/dist/worker.js +2604 -325
  527. package/dist/worker.js.map +1 -1
  528. package/dist/workflows/v3/session-relay-client.d.ts +1 -0
  529. package/dist/workflows/v3/session-relay-client.d.ts.map +1 -1
  530. package/dist/workflows/v3/session-relay-client.js +12 -3
  531. package/dist/workflows/v3/session-relay-client.js.map +1 -1
  532. package/package.json +1 -1
  533. package/dist/dashboard-web/chunks/bot-defaults-page-LTVOUDIS.js +0 -10
  534. package/dist/dashboard-web/chunks/chunk-DCS35UMD.js +0 -1
  535. package/dist/dashboard-web/chunks/chunk-MFQPCVHY.js +0 -51
  536. package/dist/dashboard-web/chunks/connectors-page-5KWDIMKC.js +0 -1
  537. package/dist/dashboard-web/chunks/groups-page-FHOKYXMW.js +0 -8
  538. package/dist/dashboard-web/chunks/overview-page-QYI3KBDD.js +0 -1
  539. package/dist/dashboard-web/chunks/sessions-page-WBP4N7D2.js +0 -6
@@ -14,7 +14,7 @@ import * as scheduler from './scheduler.js';
14
14
  import { scanMultipleProjects, describeProjectDir } from '../services/project-scanner.js';
15
15
  import { createRepoWorktree, pushWorktreeBranch } from '../services/git-worktree.js';
16
16
  import { worktreeSlugFromContextAI } from '../services/worktree-slug-ai.js';
17
- import { resolvePairedSpawnBackendType } from './persistent-backend.js';
17
+ import { isRiffBackendSession, resolvePairedSpawnBackendType } from './persistent-backend.js';
18
18
  import { buildRepoSelectCard, buildAdoptSelectCard, buildCodexAppThreadSelectCard, buildSlashListCard, getCliDisplayName, buildConfigCard, buildForkPanelCard, buildAdoptBlockedCard } from '../im/lark/card-builder.js';
19
19
  import { handleDashboardCommand } from './dashboard-command/index.js';
20
20
  import { createCliAdapterSync } from '../adapters/cli/registry.js';
@@ -24,8 +24,8 @@ import { chatAppLink, threadAppLink, normalizeBrand } from '../im/lark/lark-host
24
24
  import { claimPairing } from '../services/pairing-store.js';
25
25
  import { logger } from '../utils/logger.js';
26
26
  import { scheduleTimeZone } from '../utils/timezone.js';
27
- import { closeSession, killWorker, teardownAuthoritativePersistentBackingBeforeClose, suspendWorker, forkWorker, forkAdoptWorker, adoptSandboxBlocked, getCurrentCliVersion, postFreshStreamingCard, postPrivateSnapshotCard, resolvePrivateCardAudience, deliverEphemeralOrReply, deliverWritableTerminalCardTo, requestSessionRestart, isSessionTransferring } from './worker-pool.js';
28
- import { expandHome, getSessionWorkingDir, getProjectScanDirs, rememberLastCliInput, buildNewTopicCliInput, ensureSessionWhiteboard, getAvailableBots, } from './session-manager.js';
27
+ import { killWorker, teardownAuthoritativePersistentBackingBeforeClose, suspendWorker, forkWorker, forkAdoptWorker, adoptSandboxBlocked, getCurrentCliVersion, postFreshStreamingCard, postPrivateSnapshotCard, resolvePrivateCardAudience, deliverEphemeralOrReply, deliverWritableTerminalCardTo, closeSession as closeWorkerPoolSession, withActiveSessionKeyLock, requestSessionRestart, isSessionTransferring } from './worker-pool.js';
28
+ import { expandHome, getSessionWorkingDir, getProjectScanDirs, rememberLastCliInput, buildNewTopicCliInput, getAvailableBots, } from './session-manager.js';
29
29
  import { markInitialUserTurnPending } from './initial-user-turn.js';
30
30
  import { discoverSlashCommandsForAdapter, listMcpServerNames, supportsFilesystemCommandDiscovery } from './command-discovery.js';
31
31
  import { validateWorkingDir } from './working-dir.js';
@@ -45,7 +45,7 @@ import { CONFIG_FIELDS, findConfigField, settableFieldKeys, parseBooleanValue, a
45
45
  import { resolveCliId, findInvalidAllowedUserEntries } from '../setup/bot-config-editor.js';
46
46
  import { buildClosedSessionCard } from './closed-session-card.js';
47
47
  import { ttadkConfigModelChoices } from '../setup/cli-selection.js';
48
- import { publishAttentionPatch, publishClosedSessionPatch, announcePendingRepoSession } from './session-activity.js';
48
+ import { publishAttentionPatch, announcePendingRepoSession } from './session-activity.js';
49
49
  import { setCardMode } from '../services/card-mode-store.js';
50
50
  import { canOperate } from '../im/lark/event-dispatcher.js';
51
51
  import { buildSafeInsightReport } from '../services/insight/report.js';
@@ -53,12 +53,14 @@ import { invalidWorkingDirs } from '../utils/working-dir.js';
53
53
  import { writeRoleFile, deleteRoleFile, resolveRole, resolveRoleFile, resolveTeamRoleFile, writeTeamRoleFile, deleteTeamRoleFile, MAX_ROLE_BYTES } from './role-resolver.js';
54
54
  import { getBotCapability, setBotCapability, clearBotCapability } from '../services/bot-profile-store.js';
55
55
  import { deleteRoleProfileEntry, deleteRoleProfileIfEmpty, isValidRoleProfileId, listRoleProfileEntries, listRoleProfiles, MAX_ROLE_PROFILE_ENTRY_BYTES, readRoleProfileEntry, writeRoleProfileEntry, } from '../services/role-profile-store.js';
56
- import { sessionKey, sessionAnchorId, markRepoCardConsumed, claimCurrentRepoCard } from './types.js';
56
+ import { activeSessionKey, sessionKey, sessionAnchorId, markRepoCardConsumed } from './types.js';
57
57
  import { t, localeForBot } from '../i18n/index.js';
58
58
  import { runSkillsImCommand } from './skills/im-command.js';
59
59
  import { fetchDaemonIpc } from './daemon-ipc-auth.js';
60
60
  import { updateSessionTitle } from './session-title.js';
61
61
  import { requestAgentSessionRename } from './session-rename.js';
62
+ import { hasProtectedSessionMutationOwnership } from './session-mutation-guard.js';
63
+ import { withBotTurnMutation } from './bot-turn-mutation-gate.js';
62
64
  import { configuredRuntimeDisplayName, sessionConfiguredRuntimeDisplayName, } from './cli-runtime-display.js';
63
65
  // ─── Exported constants ──────────────────────────────────────────────────────
64
66
  // DAEMON_COMMANDS / PASSTHROUGH_COMMANDS / normalizePassthroughCommand now live
@@ -1218,24 +1220,43 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
1218
1220
  switch (cmd) {
1219
1221
  case '/close': {
1220
1222
  if (ds) {
1221
- // Capture the closed-session card BEFORE killWorker/closeSession —
1222
- // it reads the live session's identity off `ds`.
1223
- const card = buildClosedSessionCard(ds, loc);
1224
- const activeKey = sessionKey(rootId, larkAppId);
1225
- try {
1226
- await closeSession(ds.session.sessionId);
1223
+ const targetSessionId = ds.session.sessionId;
1224
+ const closed = await withBotTurnMutation(ds.larkAppId, async () => {
1225
+ // Re-resolve the exact session after all peer admissions drain. A
1226
+ // relay may have moved it to another key while this command was
1227
+ // admitted; closeSession removes its current identity, never the
1228
+ // stale root key from this message.
1229
+ const current = [...activeSessions.values()].find(candidate => candidate.session.sessionId === targetSessionId);
1230
+ if (!current)
1231
+ return undefined;
1232
+ // Capture the closed-session card BEFORE closeWorkerPoolSession —
1233
+ // it reads the live session's identity off `current`.
1234
+ const card = buildClosedSessionCard(current, localeForBot(current.larkAppId));
1235
+ try {
1236
+ // closeWorkerPoolSession proves fail-closed backing teardown
1237
+ // before mutating any registry/store state, throwing when it
1238
+ // cannot verify it. Surface that so the active record is kept
1239
+ // for retry instead of being silently dropped.
1240
+ await closeWorkerPoolSession(targetSessionId);
1241
+ }
1242
+ catch (err) {
1243
+ return { status: 'teardown_failed', err };
1244
+ }
1245
+ return { status: 'closed', current, card };
1246
+ });
1247
+ if (!closed) {
1248
+ await sessionReply(rootId, t('cmd.no_active_session', undefined, loc));
1249
+ break;
1227
1250
  }
1228
- catch (err) {
1229
- logger.error(`[${logTag}] Refused /close because backing teardown was not verified: ${err}`);
1230
- await sessionReply(rootId, `⚠️ 会话关闭失败,已保留 active 记录以便重试:${err instanceof Error ? err.message : String(err)}`);
1251
+ if (closed.status === 'teardown_failed') {
1252
+ logger.error(`[${logTag}] Refused /close because backing teardown was not verified: ${closed.err}`);
1253
+ await sessionReply(rootId, `⚠️ 会话关闭失败,已保留 active 记录以便重试:${closed.err instanceof Error ? closed.err.message : String(closed.err)}`);
1231
1254
  break;
1232
1255
  }
1233
- if (activeSessions.get(activeKey) === ds)
1234
- activeSessions.delete(activeKey);
1235
1256
  // 「会话已关闭」卡片优先「仅自己可见」:普通群里走 ephemeral 只发给执行
1236
1257
  // /close 的本人;话题群不支持 ephemeral(18053) 时回退为正常的群内可见回复
1237
1258
  // ——与流式卡片上「关闭会话」按钮的送达方式保持一致。
1238
- await deliverEphemeralOrReply(ds, message.senderId, card, 'interactive', () => sessionReply(rootId, card, 'interactive'));
1259
+ await deliverEphemeralOrReply(closed.current, message.senderId, closed.card, 'interactive', () => sessionReply(rootId, closed.card, 'interactive'));
1239
1260
  logger.info(`[${logTag}] Session closed by /close command`);
1240
1261
  }
1241
1262
  else {
@@ -1280,10 +1301,17 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
1280
1301
  break;
1281
1302
  }
1282
1303
  const closedSessionId = ds.session.sessionId;
1283
- killWorker(ds);
1284
- sessionStore.closeSession(closedSessionId);
1285
- publishClosedSessionPatch(closedSessionId, ds.session.closedAt ? Date.parse(ds.session.closedAt) : undefined);
1286
- activeSessions.delete(sessionKey(rootId, larkAppId));
1304
+ const detached = await withBotTurnMutation(ds.larkAppId, async () => {
1305
+ const current = [...activeSessions.values()].find(candidate => candidate.session.sessionId === closedSessionId);
1306
+ if (!current || !current.adoptedFrom)
1307
+ return false;
1308
+ await closeWorkerPoolSession(closedSessionId);
1309
+ return true;
1310
+ });
1311
+ if (!detached) {
1312
+ await sessionReply(rootId, t('cmd.no_active_session', undefined, loc));
1313
+ break;
1314
+ }
1287
1315
  await sessionReply(rootId, t('cmd.detach.success', undefined, loc));
1288
1316
  logger.info(`[${logTag}] Detached (adopt) by ${cmd} command`);
1289
1317
  break;
@@ -1294,6 +1322,19 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
1294
1322
  await sessionReply(rootId, t('card.action.adopt_no_restart', undefined, loc));
1295
1323
  break;
1296
1324
  }
1325
+ if (isRiffBackendSession(ds)) {
1326
+ logger.info(`[${logTag}] Rejected /restart for Riff backend session`);
1327
+ await sessionReply(rootId, t('cmd.restart.riff_unsupported', undefined, loc));
1328
+ break;
1329
+ }
1330
+ // Codex App: an accepted-but-unsettled dispatch still owns the turn
1331
+ // route. requestSessionRestart does not itself gate on dispatch
1332
+ // ownership, so reject here before the coordinator tears the worker
1333
+ // down (mirrors the card-handler restart path).
1334
+ if (hasProtectedSessionMutationOwnership(ds)) {
1335
+ await sessionReply(rootId, '当前 Codex App 仍有未结算消息,暂不能重启;请等待本轮完成或关闭会话。');
1336
+ break;
1337
+ }
1297
1338
  if (isSessionTransferring(ds)) {
1298
1339
  await sessionReply(rootId, t('cmd.session.transfer_in_progress', undefined, loc));
1299
1340
  break;
@@ -1326,22 +1367,55 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
1326
1367
  await sessionReply(rootId, t('cmd.session.transfer_in_progress', undefined, loc));
1327
1368
  break;
1328
1369
  }
1370
+ // A live Riff worker owns a remote task rooted in its original cwd.
1371
+ // killWorker/restart deliberately refuse to replace that generation,
1372
+ // so persisting a new cwd here would report success while the remote
1373
+ // task keeps running in the old directory.
1374
+ if (isRiffBackendSession(ds)) {
1375
+ await sessionReply(rootId, t('cmd.cd.riff_unsupported', undefined, loc));
1376
+ break;
1377
+ }
1378
+ // Cheap preflight avoids creating a requested directory when the
1379
+ // current FIFO already makes the switch impossible. The mutation
1380
+ // below repeats this check after draining peer admissions.
1381
+ if (hasProtectedSessionMutationOwnership(ds)) {
1382
+ await sessionReply(rootId, '当前 Codex App 仍有未结算消息,暂不能切换工作目录;请等待本轮完成或关闭会话。');
1383
+ break;
1384
+ }
1329
1385
  const validation = validateWorkingDir(targetPath, loc, { autoCreate: true });
1330
1386
  if (!validation.ok) {
1331
1387
  await sessionReply(rootId, validation.error);
1332
1388
  break;
1333
1389
  }
1334
1390
  const resolvedPath = validation.resolvedPath;
1335
- // Persistent backends can cold-resume without treating a cwd switch as
1336
- // a real session close. This also preserves a sandbox upper long enough
1337
- // for the next worker to relocate Claude's cwd-scoped transcript.
1338
- // Adopted sessions are observers of a user-owned CLI and keep the old
1339
- // detach/kill behavior; PTY falls back to killWorker, while its normal
1340
- // on-disk transcript remains available for the resume-sync preflight.
1341
- const suspended = !ds.adoptedFrom && suspendWorker(ds, 'working_dir_changed');
1342
- if (!suspended)
1343
- killWorker(ds);
1344
- repinSessionWorkingDir(ds, resolvedPath);
1391
+ const targetSessionId = ds.session.sessionId;
1392
+ const switched = await withBotTurnMutation(ds.larkAppId, async () => {
1393
+ const current = [...activeSessions.values()].find(candidate => candidate.session.sessionId === targetSessionId
1394
+ && candidate.session.status === 'active');
1395
+ if (!current)
1396
+ return 'gone';
1397
+ if (hasProtectedSessionMutationOwnership(current)) {
1398
+ return 'pending';
1399
+ }
1400
+ const suspended = !current.adoptedFrom
1401
+ && suspendWorker(current, 'working_dir_changed');
1402
+ if (!suspended)
1403
+ killWorker(current);
1404
+ repinSessionWorkingDir(current, resolvedPath);
1405
+ // cwd 变了,riff 多仓 stamp(选择卡多选时写入)随之失效——保留会让下次
1406
+ // refork 仍按旧仓库组合推导、无视新目录。
1407
+ current.session.riffRepoDirs = undefined;
1408
+ sessionStore.updateSession(current.session);
1409
+ return 'switched';
1410
+ });
1411
+ if (switched === 'pending') {
1412
+ await sessionReply(rootId, '当前 Codex App 仍有未结算消息,暂不能切换工作目录;请等待本轮完成或关闭会话。');
1413
+ break;
1414
+ }
1415
+ if (switched === 'gone') {
1416
+ await sessionReply(rootId, t('cmd.no_active_session', undefined, loc));
1417
+ break;
1418
+ }
1345
1419
  if (validation.created) {
1346
1420
  await sessionReply(rootId, t('cmd.cd.created_switched', { path: resolvedPath }, loc));
1347
1421
  }
@@ -1385,148 +1459,156 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
1385
1459
  break;
1386
1460
  }
1387
1461
  case '/repo': {
1462
+ // A live Riff generation must finish the explicit /close protocol before
1463
+ // its anchor can be reused. The generic repo-switch path closes and
1464
+ // immediately reforks; if remote cancellation fails, that would fall
1465
+ // through to the double-fork kill and orphan the remote task.
1466
+ if (ds && !ds.pendingRepo && isRiffBackendSession(ds)) {
1467
+ await sessionReply(rootId, t('cmd.cd.riff_unsupported', undefined, loc));
1468
+ logger.warn(`[${logTag}] Repo switch refused: Riff session requires explicit close before replacement`);
1469
+ break;
1470
+ }
1388
1471
  const repoArg = message.content.replace(/^\/repo\s*/, '').trim();
1472
+ if (ds && !ds.pendingRepo
1473
+ && hasProtectedSessionMutationOwnership(ds)) {
1474
+ await sessionReply(rootId, '当前 Codex App 仍有未结算消息,暂不能切换仓库;请等待本轮完成或关闭会话。');
1475
+ break;
1476
+ }
1389
1477
  // First-spawn fork: consume the buffered prompt/attachments and start the
1390
1478
  // CLI in whatever workingDir is currently set on the session. Shared by
1391
1479
  // `commitRepoSelection` (a repo was named) and the bare-`/repo` launch
1392
1480
  // (use the default workingDir) — both only run while `pendingRepo`.
1393
- const forkPendingCli = async (replyText, claimAlready = false) => {
1394
- if (!claimAlready) {
1395
- if (ds.pendingRepoCommitInFlight) {
1396
- await sessionReply(rootId, t('cmd.repo.worktree_in_progress', undefined, loc));
1481
+ const forkPendingCli = async (replyText, selection) => {
1482
+ const targetSessionId = ds.session.sessionId;
1483
+ const started = await withBotTurnMutation(ds.larkAppId, async () => {
1484
+ const current = [...activeSessions.values()].find(candidate => candidate.session.sessionId === targetSessionId
1485
+ && candidate.session.status === 'active');
1486
+ if (!current || current !== ds || !current.pendingRepo)
1397
1487
  return false;
1488
+ if (selection) {
1489
+ current.workingDir = selection.path;
1490
+ current.session.workingDir = selection.path;
1491
+ current.session.riffRepoDirs = selection.riffRepoDirs;
1492
+ sessionStore.updateSession(current.session);
1398
1493
  }
1399
- ds.pendingRepoCommitInFlight = true;
1400
- }
1401
- const selfBot = getBot(ds.larkAppId);
1402
- const botCfg = selfBot.config;
1403
- const commitGenSessionId = ds.session.sessionId;
1404
- let committed = false;
1405
- try {
1406
- // Keep pendingRepo=true while prompt context is awaited. Incoming
1407
- // messages stay in the same buffer and card selections see the
1408
- // shared in-flight claim instead of treating this as a repo switch.
1409
- const initiallyBuffered = (ds.pendingPrompt?.trim().length ?? 0) > 0 ||
1410
- (ds.pendingAttachments?.length ?? 0) > 0 ||
1411
- (ds.pendingFollowUps?.length ?? 0) > 0 ||
1412
- ds.pendingChatContext !== undefined;
1413
- const availableBots = initiallyBuffered
1414
- ? await getAvailableBots(ds.larkAppId, ds.chatId)
1415
- : [];
1416
- const stillActive = activeSessions.get(sessionKey(rootId, larkAppId)) === ds;
1417
- if (!stillActive || ds.session.sessionId !== commitGenSessionId ||
1418
- !ds.pendingRepo || (ds.worker && !ds.worker.killed)) {
1419
- logger.warn(`[${logTag}] Session changed while preparing the pending CLI (${commitGenSessionId} ${ds.session.sessionId}, active=${stillActive}, pending=${!!ds.pendingRepo}, worker=${!!ds.worker})`);
1420
- return false;
1421
- }
1422
- const pendingPrompt = ds.pendingPrompt ?? '';
1423
- const pendingRawInput = ds.pendingRawInput;
1424
- const hasBufferedInput = pendingPrompt.trim().length > 0 ||
1425
- (ds.pendingAttachments?.length ?? 0) > 0 ||
1426
- (ds.pendingFollowUps?.length ?? 0) > 0 ||
1427
- ds.pendingChatContext !== undefined;
1428
- // Nothing to submit at all (bare `/repo`: the message IS the command).
1429
- // The CLI boots idle, so the user's NEXT real message is its first
1430
- // turn and must carry the full new-topic opening — see
1431
- // markInitialUserTurnPending below.
1432
- const emptyStart = !pendingRawInput && !hasBufferedInput;
1433
- if (hasBufferedInput)
1434
- ensureSessionWhiteboard(ds);
1435
- const wrappedInput = hasBufferedInput
1436
- ? buildNewTopicCliInput(pendingPrompt, ds.session.sessionId, ds.session.cliId ?? botCfg.cliId, ds.session.cliPathOverride ?? botCfg.cliPathOverride, ds.pendingAttachments, ds.pendingMentions, availableBots, ds.pendingFollowUps, { name: selfBot.botName, openId: selfBot.botOpenId }, loc, ds.pendingSender, {
1494
+ const selfBot = getBot(current.larkAppId);
1495
+ const botCfg = selfBot.config;
1496
+ const pendingPrompt = current.pendingPrompt ?? '';
1497
+ const pendingRawInput = current.pendingRawInput;
1498
+ const hasBufferedInput = pendingPrompt.trim().length > 0
1499
+ || current.pendingCodexAppText !== undefined
1500
+ || (current.pendingAttachments?.length ?? 0) > 0
1501
+ || (current.pendingFollowUps?.length ?? 0) > 0
1502
+ || current.pendingChatContext !== undefined;
1503
+ let wrappedInput;
1504
+ if (hasBufferedInput) {
1505
+ const { buildNewTopicCliInput: buildInput, ensureSessionWhiteboard, getAvailableBots } = await import('./session-manager.js');
1506
+ ensureSessionWhiteboard(current);
1507
+ const availableBots = await getAvailableBots(current.larkAppId, current.chatId);
1508
+ // Detached lifecycle work can still close/replace while the
1509
+ // roster lookup awaits. Never fork the captured generation.
1510
+ if (current.session.status !== 'active'
1511
+ || [...activeSessions.values()].find(candidate => candidate.session.sessionId === targetSessionId) !== current
1512
+ || !current.pendingRepo)
1513
+ return false;
1514
+ wrappedInput = buildInput(pendingPrompt, current.session.sessionId, current.session.cliId ?? botCfg.cliId, current.session.cliPathOverride ?? botCfg.cliPathOverride, current.pendingAttachments, current.pendingMentions, availableBots, current.pendingFollowUps, { name: selfBot.botName, openId: selfBot.botOpenId }, loc, current.pendingSender, {
1437
1515
  larkAppId,
1438
- chatId: ds.chatId,
1439
- whiteboardId: ds.session.whiteboardId,
1440
- substituteTrigger: ds.pendingSubstituteTrigger,
1441
- codexAppText: ds.pendingCodexAppText,
1442
- codexAppApplicationContext: ds.pendingCodexAppApplicationContext,
1443
- codexAppMessageContext: ds.pendingCodexAppMessageContext,
1444
- codexAppFollowUps: ds.pendingCodexAppFollowUps,
1445
- codexAppFollowUpContexts: ds.pendingCodexAppFollowUpContexts,
1446
- chatContext: ds.pendingChatContext,
1447
- })
1448
- : { content: '' };
1449
- const pendingTurnId = ds.pendingTurnId;
1450
- const previousPendingFollowUpInput = ds.pendingFollowUpInput;
1451
- if (pendingRawInput && hasBufferedInput) {
1452
- ds.pendingFollowUpInput = {
1453
- userPrompt: ds.pendingCodexAppText !== undefined || ds.pendingCodexAppFollowUps
1454
- ? [ds.pendingCodexAppText ?? '', ...(ds.pendingCodexAppFollowUps ?? [])].filter(Boolean).join('\n\n')
1455
- : pendingPrompt || ds.pendingFollowUps?.join('\n\n') || '',
1516
+ chatId: current.chatId,
1517
+ whiteboardId: current.session.whiteboardId,
1518
+ substituteTrigger: current.pendingSubstituteTrigger,
1519
+ codexAppText: current.pendingCodexAppText,
1520
+ codexAppApplicationContext: current.pendingCodexAppApplicationContext,
1521
+ codexAppMessageContext: current.pendingCodexAppMessageContext,
1522
+ codexAppFollowUps: current.pendingCodexAppFollowUps,
1523
+ codexAppFollowUpContexts: current.pendingCodexAppFollowUpContexts,
1524
+ chatContext: current.pendingChatContext,
1525
+ });
1526
+ }
1527
+ if (pendingRawInput && hasBufferedInput && wrappedInput) {
1528
+ current.pendingFollowUpInput = {
1529
+ userPrompt: current.pendingCodexAppText !== undefined || current.pendingCodexAppFollowUps
1530
+ ? [current.pendingCodexAppText ?? '', ...(current.pendingCodexAppFollowUps ?? [])].filter(Boolean).join('\n\n')
1531
+ : pendingPrompt || current.pendingFollowUps?.join('\n\n') || '',
1456
1532
  cliInput: wrappedInput.content,
1457
- ...(ds.pendingFollowUpTurnId ? { turnId: ds.pendingFollowUpTurnId } : {}),
1458
- ...((ds.session.cliId ?? botCfg.cliId) === 'codex-app' && botCfg.codexAppCleanInput === true && wrappedInput.codexAppInput
1533
+ ...((current.pendingFollowUpTurnIds?.at(-1) ?? current.pendingFollowUpTurnId)
1534
+ ? { turnId: current.pendingFollowUpTurnIds?.at(-1) ?? current.pendingFollowUpTurnId }
1535
+ : {}),
1536
+ ...((current.session.cliId ?? botCfg.cliId) === 'codex-app' && botCfg.codexAppCleanInput === true && wrappedInput.codexAppInput
1459
1537
  ? { codexAppInput: wrappedInput.codexAppInput }
1460
1538
  : {}),
1461
1539
  codexAppInputGateFrozen: true,
1462
1540
  };
1463
1541
  }
1464
- ds.pendingRepo = false;
1465
- publishAttentionPatch(ds);
1466
- try {
1467
- if (pendingRawInput)
1468
- forkWorker(ds, '', false);
1469
- else if (pendingTurnId && hasBufferedInput)
1470
- forkWorker(ds, wrappedInput, { turnId: pendingTurnId });
1471
- else if (hasBufferedInput)
1472
- forkWorker(ds, wrappedInput);
1473
- else
1474
- forkWorker(ds, '', false);
1475
- }
1476
- catch (e) {
1477
- ds.pendingRepo = true;
1478
- ds.pendingFollowUpInput = previousPendingFollowUpInput;
1479
- publishAttentionPatch(ds);
1480
- throw e;
1481
- }
1482
- if (pendingRawInput || hasBufferedInput) {
1483
- rememberLastCliInput(ds, pendingRawInput ?? pendingPrompt, pendingRawInput ?? wrappedInput);
1484
- }
1485
- // Durable, one-shot: the CLI is up but has never received a real user
1486
- // turn, so the next business message must be built as a NEW TOPIC
1487
- // (routing + built-in skill discovery + identity), not a follow-up.
1488
- // Set after the fork so a throwing fork leaves the session untouched.
1542
+ if (pendingRawInput)
1543
+ rememberLastCliInput(current, pendingRawInput, pendingRawInput);
1544
+ else if (hasBufferedInput && wrappedInput)
1545
+ rememberLastCliInput(current, pendingPrompt, wrappedInput);
1546
+ // forkWorker performs the synchronous pre-accept/write-ahead work.
1547
+ // Keep the opening reservation and every buffered field intact if
1548
+ // that step throws, so a failed launch cannot silently consume the
1549
+ // first user turn or expose this worker:null owner as scratch.
1550
+ const pendingTurnId = current.pendingTurnId
1551
+ ?? current.session.pendingRepoSetup?.turnId;
1552
+ // Nothing to submit at all (bare `/repo`: the message IS the
1553
+ // command). The CLI boots idle, so the user's NEXT real message is
1554
+ // its first turn and must carry the full new-topic opening — see
1555
+ // markInitialUserTurnPending below.
1556
+ const emptyStart = !pendingRawInput && !hasBufferedInput;
1557
+ forkWorker(current, pendingRawInput ? '' : (wrappedInput ?? ''), !pendingRawInput && pendingTurnId ? { turnId: pendingTurnId } : false);
1558
+ current.pendingRepo = false;
1559
+ current.pendingRepoCommitInFlight = true;
1560
+ // Queued activation ownership lasts through adapter submission.
1561
+ // These source buffers were folded into opening N; clear them but
1562
+ // keep the gate so later inbounds enter the exact post-ACK FIFO.
1563
+ current.initialStartPending = current.session.queuedActivationPending === true;
1564
+ // Durable, one-shot: an empty-started CLI has never received a real
1565
+ // user turn, so the next business message must be built as a NEW
1566
+ // TOPIC (routing + built-in skill discovery + identity), not a
1567
+ // follow-up. Set after the fork so a throwing fork leaves it clean.
1489
1568
  if (emptyStart)
1490
- markInitialUserTurnPending(ds);
1491
- ds.pendingPrompt = undefined;
1492
- ds.pendingCodexAppText = undefined;
1493
- ds.pendingCodexAppApplicationContext = undefined;
1494
- ds.pendingCodexAppMessageContext = undefined;
1495
- ds.pendingChatContext = undefined;
1496
- ds.pendingAttachments = undefined;
1497
- ds.pendingMentions = undefined;
1498
- ds.pendingSubstituteTrigger = undefined;
1499
- ds.pendingSender = undefined;
1500
- ds.pendingFollowUps = undefined;
1501
- ds.pendingCodexAppFollowUps = undefined;
1502
- ds.pendingCodexAppFollowUpContexts = undefined;
1503
- ds.pendingFollowUpTurnId = undefined;
1504
- ds.pendingTurnId = undefined;
1505
- committed = true;
1506
- // Local one-shot consume BEFORE network awaits. Feishu withdraw is
1507
- // best-effort; stale card clicks must be rejected via this mark even
1508
- // if sessionReply throws or deleteMessage lags/fails.
1509
- const cardToWithdraw = ds.repoCardMessageId;
1510
- markRepoCardConsumed(ds, cardToWithdraw);
1511
- ds.repoCardMessageId = undefined;
1512
- // Hold the claim through confirmation + best-effort card withdraw.
1569
+ markInitialUserTurnPending(current);
1570
+ publishAttentionPatch(current);
1571
+ current.pendingPrompt = undefined;
1572
+ current.pendingCodexAppText = undefined;
1573
+ current.pendingCodexAppApplicationContext = undefined;
1574
+ current.pendingCodexAppMessageContext = undefined;
1575
+ current.pendingChatContext = undefined;
1576
+ current.pendingAttachments = undefined;
1577
+ current.pendingMentions = undefined;
1578
+ current.pendingSubstituteTrigger = undefined;
1579
+ current.pendingSender = undefined;
1580
+ current.pendingFollowUps = undefined;
1581
+ current.pendingFollowUpTurnId = undefined;
1582
+ current.pendingFollowUpTurnIds = undefined;
1583
+ current.pendingCodexAppFollowUps = undefined;
1584
+ current.pendingCodexAppFollowUpContexts = undefined;
1585
+ current.pendingCodexAppFollowUpGateAccepted = undefined;
1586
+ current.pendingTurnId = undefined;
1587
+ const cardToWithdraw = current.repoCardMessageId;
1588
+ markRepoCardConsumed(current, cardToWithdraw);
1589
+ current.repoCardMessageId = undefined;
1590
+ return { current, cardToWithdraw };
1591
+ });
1592
+ if (!started)
1593
+ return false;
1594
+ try {
1513
1595
  try {
1514
1596
  await sessionReply(rootId, replyText);
1515
1597
  }
1516
1598
  catch (e) {
1517
1599
  logger.warn(`[${logTag}] Confirm reply after pending repo commit failed: ${e instanceof Error ? e.message : e}`);
1518
1600
  }
1519
- if (cardToWithdraw) {
1601
+ if (started.cardToWithdraw) {
1520
1602
  try {
1521
- await deleteMessage(ds.larkAppId, cardToWithdraw);
1603
+ await deleteMessage(started.current.larkAppId, started.cardToWithdraw);
1522
1604
  }
1523
1605
  catch { /* best-effort */ }
1524
1606
  }
1525
1607
  }
1526
1608
  finally {
1527
- ds.pendingRepoCommitInFlight = false;
1609
+ started.current.pendingRepoCommitInFlight = false;
1528
1610
  }
1529
- return committed;
1611
+ return true;
1530
1612
  };
1531
1613
  // Shared commit path for an already-resolved repo: update the session's
1532
1614
  // working dir, then either fork into the pending CLI (first spawn) or
@@ -1534,27 +1616,11 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
1534
1616
  // numeric `/repo <N>` form and the `/repo <path|name>` form.
1535
1617
  const commitRepoSelection = async (selectedPath, displayName, how) => {
1536
1618
  if (ds.pendingRepo) {
1537
- if (ds.pendingRepoCommitInFlight) {
1538
- await sessionReply(rootId, t('cmd.repo.worktree_in_progress', undefined, loc));
1619
+ // First spawn: the cwd pin and fork are one exclusive commit. Two
1620
+ // simultaneous selections cannot make A reply while forking B's cwd.
1621
+ const started = await forkPendingCli(t('cmd.repo.selected_in_pending', { name: displayName }, loc), { path: selectedPath, riffRepoDirs: undefined });
1622
+ if (!started)
1539
1623
  return false;
1540
- }
1541
- ds.pendingRepoCommitInFlight = true;
1542
- try {
1543
- // Claim before changing cwd so a card selection cannot overwrite
1544
- // this choice while the pending prompt is prepared.
1545
- ds.workingDir = selectedPath;
1546
- ds.session.workingDir = selectedPath;
1547
- ds.session.riffRepoDirs = undefined;
1548
- sessionStore.updateSession(ds.session);
1549
- // forkPendingCli owns claim release + confirm reply + card withdraw.
1550
- if (!await forkPendingCli(t('cmd.repo.selected_in_pending', { name: displayName }, loc), true)) {
1551
- return false;
1552
- }
1553
- }
1554
- catch (e) {
1555
- ds.pendingRepoCommitInFlight = false;
1556
- throw e;
1557
- }
1558
1624
  }
1559
1625
  else {
1560
1626
  // Safety net: a mid-session `/repo` switch closes the running
@@ -1588,56 +1654,85 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
1588
1654
  // card), so `claude --resume` later would reopen the old context in
1589
1655
  // the new repo's cwd. The new repo is pinned onto the fresh session
1590
1656
  // below instead.
1591
- const claimedCard = claimCurrentRepoCard(ds, undefined);
1592
- const oldSession = ds.session;
1593
- const oldSessionId = oldSession.sessionId;
1594
- const closedCard = buildClosedSessionCard(ds, loc);
1595
- killWorker(ds);
1596
- sessionStore.closeSession(ds.session.sessionId);
1597
- publishClosedSessionPatch(ds.session.sessionId, ds.session.closedAt ? Date.parse(ds.session.closedAt) : undefined);
1598
- await deliverEphemeralOrReply(ds, message.senderId, closedCard, 'interactive', () => sessionReply(rootId, closedCard, 'interactive'));
1599
- const stillOwnsGeneration = activeSessions.get(sessionKey(rootId, larkAppId)) === ds
1600
- && ds.session === oldSession;
1601
- if (!stillOwnsGeneration) {
1602
- logger.warn(`[${logTag}] Repo switch cancelled after closing ${oldSessionId.substring(0, 8)}; routing generation changed during card delivery`);
1657
+ const targetSessionId = ds.session.sessionId;
1658
+ const switched = await withBotTurnMutation(ds.larkAppId, async () => {
1659
+ const candidate = [...activeSessions.values()].find(candidate => candidate.session.sessionId === targetSessionId);
1660
+ if (!candidate || candidate !== ds || candidate.session.status !== 'active') {
1661
+ return { ok: false, error: 'session_replaced' };
1662
+ }
1663
+ const key = activeSessionKey(candidate);
1664
+ return withActiveSessionKeyLock(activeSessions, key, async () => {
1665
+ // Resume/scheduler/dashboard creators use this same key lock
1666
+ // without joining the bot admission gate. Re-resolve after the
1667
+ // lock wait, then keep it across close -> replacement publish.
1668
+ const current = [...activeSessions.values()].find(owner => owner.session.sessionId === targetSessionId);
1669
+ if (!current || current !== candidate
1670
+ || activeSessions.get(key) !== current
1671
+ || current.session.status !== 'active') {
1672
+ return { ok: false, error: 'session_replaced' };
1673
+ }
1674
+ if (hasProtectedSessionMutationOwnership(current)) {
1675
+ return { ok: false, error: 'dispatch_pending' };
1676
+ }
1677
+ const closedCard = buildClosedSessionCard(current, loc);
1678
+ const oldSession = current.session;
1679
+ await closeWorkerPoolSession(targetSessionId);
1680
+ // The key lock excludes every sanctioned creator. A direct
1681
+ // lifecycle callback may still have published unexpectedly;
1682
+ // fail closed instead of overwriting that first owner.
1683
+ if (activeSessions.get(key) === current)
1684
+ activeSessions.delete(key);
1685
+ if (activeSessions.has(key)) {
1686
+ return { ok: false, error: 'session_replaced' };
1687
+ }
1688
+ const cardToWithdraw = current.repoCardMessageId;
1689
+ markRepoCardConsumed(current, cardToWithdraw);
1690
+ current.repoCardMessageId = undefined;
1691
+ const session = sessionStore.createSession(current.chatId, current.scope === 'chat' ? oldSession.rootMessageId : rootId, displayName, current.chatType, current.scope);
1692
+ current.session = session;
1693
+ current.lastUserPrompt = undefined;
1694
+ current.lastCliInput = undefined;
1695
+ current.workingDir = selectedPath;
1696
+ session.workingDir = selectedPath;
1697
+ session.larkAppId = current.larkAppId;
1698
+ session.chatDisplayName = oldSession.chatDisplayName;
1699
+ session.ownerOpenId = oldSession.ownerOpenId;
1700
+ session.creatorOpenId = oldSession.creatorOpenId;
1701
+ session.lastCallerOpenId = oldSession.lastCallerOpenId;
1702
+ sessionStore.updateSession(session);
1703
+ current.hasHistory = false;
1704
+ activeSessions.set(key, current);
1705
+ forkWorker(current, '', false);
1706
+ // Brand-new CLI in a brand-new session record: it has never
1707
+ // seen the botmux opening context either, so the next real
1708
+ // business message is its new-topic first turn (same invariant
1709
+ // as the pending path).
1710
+ markInitialUserTurnPending(current);
1711
+ return { ok: true, current, closedCard, cardToWithdraw };
1712
+ });
1713
+ });
1714
+ if (!switched.ok) {
1715
+ if (switched.error === 'dispatch_pending') {
1716
+ await sessionReply(rootId, '当前 Codex App 仍有未结算消息,暂不能切换仓库;请等待本轮完成或关闭会话。');
1717
+ }
1718
+ else {
1719
+ logger.warn(`[${logTag}] Repo switch aborted because the session was replaced`);
1720
+ }
1603
1721
  return false;
1604
1722
  }
1605
- // `rootId` is the routing anchor. For chat-scope sessions it is the
1606
- // `oc_...` chat id, not the traceable `om_...` message root stored on
1607
- // Session. Preserve the old identity and explicitly persist scope so
1608
- // repo switches cannot turn a chat session into a legacy scope-less
1609
- // record that the CLI later mistakes for a thread.
1610
- const session = sessionStore.createSession(ds.chatId, ds.scope === 'chat' ? oldSession.rootMessageId : rootId, displayName, ds.chatType, ds.scope);
1611
- ds.session = session;
1612
- ds.lastUserPrompt = undefined;
1613
- ds.lastCliInput = undefined;
1614
- ds.workingDir = selectedPath;
1615
- ds.session.workingDir = selectedPath;
1616
- ds.session.larkAppId = ds.larkAppId;
1617
- ds.session.chatDisplayName = oldSession.chatDisplayName;
1618
- ds.session.ownerOpenId = oldSession.ownerOpenId;
1619
- ds.session.creatorOpenId = oldSession.creatorOpenId;
1620
- ds.session.lastCallerOpenId = oldSession.lastCallerOpenId;
1621
- sessionStore.updateSession(ds.session);
1622
- ds.hasHistory = false;
1623
- forkWorker(ds, '', false);
1624
- // Brand-new CLI in a brand-new session record: it has never seen the
1625
- // botmux opening context either, so the next real business message
1626
- // is its new-topic first turn (same invariant as the pending path).
1627
- markInitialUserTurnPending(ds);
1628
- try {
1629
- await sessionReply(rootId, t('cmd.repo.switched_to', { name: displayName }, loc));
1630
- }
1631
- catch (e) {
1632
- logger.warn(`[${logTag}] Confirm reply after mid-session repo switch failed: ${e instanceof Error ? e.message : e}`);
1633
- }
1634
- if (claimedCard) {
1723
+ await deliverEphemeralOrReply(switched.current, message.senderId, switched.closedCard, 'interactive', () => sessionReply(rootId, switched.closedCard, 'interactive'));
1724
+ await sessionReply(rootId, t('cmd.repo.switched_to', { name: displayName }, loc));
1725
+ if (switched.cardToWithdraw) {
1635
1726
  try {
1636
- await deleteMessage(ds.larkAppId, claimedCard);
1727
+ await deleteMessage(switched.current.larkAppId, switched.cardToWithdraw);
1637
1728
  }
1638
1729
  catch { /* best-effort */ }
1639
1730
  }
1640
1731
  }
1732
+ if (ds.repoCardMessageId) {
1733
+ deleteMessage(ds.larkAppId, ds.repoCardMessageId);
1734
+ ds.repoCardMessageId = undefined;
1735
+ }
1641
1736
  logger.info(`[${logTag}] Repo selected via ${how}: ${selectedPath}`);
1642
1737
  return true;
1643
1738
  };
@@ -3776,6 +3871,21 @@ async function blockTakeoverWhilePendingRepo(ds, sessionReply) {
3776
3871
  logger.info(`[${tag(ds)}] Takeover refused: session still on pendingRepo gate — posted close-session card`);
3777
3872
  return true;
3778
3873
  }
3874
+ /**
3875
+ * A live Riff worker cannot be replaced through the generic adopt/import
3876
+ * refork path: that path sends a request-less close and then kills the local
3877
+ * worker, while Riff requires its remote task to finish the explicit
3878
+ * prepare/commit close protocol first. Refuse before target validation or any
3879
+ * persisted ownership mutation so the original lineage stays recoverable.
3880
+ */
3881
+ async function blockRiffTakeover(ds, sessionReply) {
3882
+ if (!isRiffBackendSession(ds))
3883
+ return false;
3884
+ const loc = localeForBot(ds.larkAppId);
3885
+ await sessionReply(sessionAnchorId(ds), t('cmd.takeover.riff_unsupported', undefined, loc));
3886
+ logger.warn(`[${tag(ds)}] Takeover refused: Riff session requires explicit close before replacement`);
3887
+ return true;
3888
+ }
3779
3889
  export async function startCodexAppThreadSession(thread, ds, deps, larkAppId) {
3780
3890
  const sessionReply = (rid, content, msgType) => deps.sessionReply(rid, content, msgType, larkAppId);
3781
3891
  const loc = localeForBot(ds.larkAppId ?? larkAppId);
@@ -3784,22 +3894,43 @@ export async function startCodexAppThreadSession(thread, ds, deps, larkAppId) {
3784
3894
  await sessionReply(sessionAnchorId(ds), t('cmd.session.transfer_in_progress', undefined, loc));
3785
3895
  return;
3786
3896
  }
3897
+ if (await blockRiffTakeover(ds, sessionReply))
3898
+ return;
3787
3899
  if (await blockTakeoverWhilePendingRepo(ds, sessionReply))
3788
3900
  return;
3789
- ds.adoptedFrom = undefined;
3790
- ds.workingDir = thread.cwd;
3791
- ds.hasHistory = true;
3792
- ds.currentTurnTitle = undefined;
3793
- ds.lastScreenContent = undefined;
3794
- ds.lastScreenStatus = undefined;
3795
- ds.session.workingDir = thread.cwd;
3796
- ds.session.title = `Codex App: ${title}`;
3797
- ds.session.cliId = 'codex-app';
3798
- ds.session.cliSessionId = thread.threadId;
3799
- ds.session.adoptedFrom = undefined;
3800
- sessionStore.updateSession(ds.session);
3801
- forkWorker(ds, '', true);
3802
- await sessionReply(sessionAnchorId(ds), t('cmd.codex_app_adopt.success', { title }, loc));
3901
+ const targetSessionId = ds.session.sessionId;
3902
+ const switched = await withBotTurnMutation(ds.larkAppId, async () => {
3903
+ const current = [...deps.activeSessions.values()].find(candidate => candidate.session.sessionId === targetSessionId
3904
+ && candidate.session.status === 'active');
3905
+ if (!current || current !== ds)
3906
+ return { status: 'gone' };
3907
+ if (hasProtectedSessionMutationOwnership(current)) {
3908
+ return { status: 'pending', anchor: sessionAnchorId(current) };
3909
+ }
3910
+ current.adoptedFrom = undefined;
3911
+ current.workingDir = thread.cwd;
3912
+ current.hasHistory = true;
3913
+ current.currentTurnTitle = undefined;
3914
+ current.lastScreenContent = undefined;
3915
+ current.lastScreenStatus = undefined;
3916
+ current.session.workingDir = thread.cwd;
3917
+ current.session.title = `Codex App: ${title}`;
3918
+ current.session.cliId = 'codex-app';
3919
+ current.session.cliSessionId = thread.threadId;
3920
+ current.session.adoptedFrom = undefined;
3921
+ sessionStore.updateSession(current.session);
3922
+ forkWorker(current, '', true);
3923
+ return { status: 'switched', anchor: sessionAnchorId(current) };
3924
+ });
3925
+ if (switched.status === 'gone') {
3926
+ await sessionReply(sessionAnchorId(ds), t('cmd.no_active_session', undefined, loc));
3927
+ return;
3928
+ }
3929
+ if (switched.status === 'pending') {
3930
+ await sessionReply(switched.anchor, '当前 Codex App 仍有未结算消息,不能切换原生 thread;请等待本轮完成或先关闭会话。');
3931
+ return;
3932
+ }
3933
+ await sessionReply(switched.anchor, t('cmd.codex_app_adopt.success', { title }, loc));
3803
3934
  }
3804
3935
  export async function startAdoptSession(target, ds, deps, larkAppId) {
3805
3936
  const sessionReply = (rid, content, msgType) => deps.sessionReply(rid, content, msgType, larkAppId);
@@ -3808,6 +3939,8 @@ export async function startAdoptSession(target, ds, deps, larkAppId) {
3808
3939
  await sessionReply(sessionAnchorId(ds), t('cmd.session.transfer_in_progress', undefined, loc));
3809
3940
  return;
3810
3941
  }
3942
+ if (await blockRiffTakeover(ds, sessionReply))
3943
+ return;
3811
3944
  const zellij = isZellijTarget(target);
3812
3945
  if (!zellij && target.source === 'herdr' && target.herdrSessionName && target.herdrAgentName) {
3813
3946
  const occupied = [...deps.activeSessions.values()].some(active => {
@@ -3852,29 +3985,48 @@ export async function startAdoptSession(target, ds, deps, larkAppId) {
3852
3985
  }
3853
3986
  const project = target.cwd.split('/').pop() || target.cwd;
3854
3987
  const pane = zellij ? `${target.zellijSession}/${target.zellijPaneId}` : adoptTargetLabel(target);
3855
- ds.workingDir = target.cwd;
3856
- ds.session.workingDir = target.cwd;
3857
- ds.session.title = `Adopt: ${project}`;
3858
- ds.adoptedFrom = {
3859
- source: zellij ? 'zellij' : target.source,
3860
- tmuxTarget: zellij ? undefined : target.tmuxTarget,
3861
- zellijSession: zellij ? target.zellijSession : undefined,
3862
- zellijPaneId: zellij ? target.zellijPaneId : undefined,
3863
- herdrSessionName: zellij ? undefined : target.herdrSessionName,
3864
- herdrTarget: zellij ? undefined : target.herdrTarget,
3865
- herdrPaneId: zellij ? undefined : target.herdrPaneId,
3866
- herdrAgentName: zellij ? undefined : target.herdrAgentName,
3867
- herdrTerminalId: zellij ? undefined : target.herdrTerminalId,
3868
- originalCliPid: target.cliPid,
3869
- sessionId: target.sessionId,
3870
- cliId: target.cliId,
3871
- cwd: target.cwd,
3872
- paneCols: target.paneCols,
3873
- paneRows: target.paneRows,
3874
- };
3875
- ds.session.adoptedFrom = { ...ds.adoptedFrom };
3876
- sessionStore.updateSession(ds.session);
3877
- forkAdoptWorker(ds);
3988
+ const targetSessionId = ds.session.sessionId;
3989
+ const adopted = await withBotTurnMutation(ds.larkAppId, async () => {
3990
+ const current = [...deps.activeSessions.values()].find(candidate => candidate.session.sessionId === targetSessionId
3991
+ && candidate.session.status === 'active');
3992
+ if (!current || current !== ds)
3993
+ return { status: 'gone' };
3994
+ if (hasProtectedSessionMutationOwnership(current)) {
3995
+ return { status: 'pending', anchor: sessionAnchorId(current) };
3996
+ }
3997
+ current.workingDir = target.cwd;
3998
+ current.session.workingDir = target.cwd;
3999
+ current.session.title = `Adopt: ${project}`;
4000
+ current.adoptedFrom = {
4001
+ source: zellij ? 'zellij' : target.source,
4002
+ tmuxTarget: zellij ? undefined : target.tmuxTarget,
4003
+ zellijSession: zellij ? target.zellijSession : undefined,
4004
+ zellijPaneId: zellij ? target.zellijPaneId : undefined,
4005
+ herdrSessionName: zellij ? undefined : target.herdrSessionName,
4006
+ herdrTarget: zellij ? undefined : target.herdrTarget,
4007
+ herdrPaneId: zellij ? undefined : target.herdrPaneId,
4008
+ herdrAgentName: zellij ? undefined : target.herdrAgentName,
4009
+ herdrTerminalId: zellij ? undefined : target.herdrTerminalId,
4010
+ originalCliPid: target.cliPid,
4011
+ sessionId: target.sessionId,
4012
+ cliId: target.cliId,
4013
+ cwd: target.cwd,
4014
+ paneCols: target.paneCols,
4015
+ paneRows: target.paneRows,
4016
+ };
4017
+ current.session.adoptedFrom = { ...current.adoptedFrom };
4018
+ sessionStore.updateSession(current.session);
4019
+ forkAdoptWorker(current);
4020
+ return { status: 'adopted', anchor: sessionAnchorId(current) };
4021
+ });
4022
+ if (adopted.status === 'gone') {
4023
+ await sessionReply(sessionAnchorId(ds), t('cmd.no_active_session', undefined, loc));
4024
+ return;
4025
+ }
4026
+ if (adopted.status === 'pending') {
4027
+ await sessionReply(adopted.anchor, '当前 Codex App 仍有未结算消息,不能切换到外部会话;请等待本轮完成或先关闭会话。');
4028
+ return;
4029
+ }
3878
4030
  const cliName = sessionCliDisplayName(ds);
3879
4031
  await sessionReply(sessionAnchorId(ds), t('cmd.adopt.success', { cliName, project, pane }, loc));
3880
4032
  }
@@ -3931,18 +4083,38 @@ export async function startResumeImportSession(target, ds, deps, larkAppId) {
3931
4083
  await sessionReply(sessionAnchorId(ds), t('cmd.session.transfer_in_progress', undefined, loc));
3932
4084
  return;
3933
4085
  }
4086
+ if (await blockRiffTakeover(ds, sessionReply))
4087
+ return;
3934
4088
  if (await blockTakeoverWhilePendingRepo(ds, sessionReply))
3935
4089
  return;
3936
- ds.workingDir = target.cwd;
3937
- ds.session.workingDir = target.cwd;
3938
- ds.session.cliSessionId = target.cliSessionId;
3939
- ds.session.title = target.title || `Import: ${project}`;
3940
- // Resume sandbox decision is left to forkWorker (resume=true → not sandboxed,
3941
- // matching restore semantics). Mark history so the session is treated as a
3942
- // resume, not a fresh spawn.
3943
- ds.hasHistory = true;
3944
- sessionStore.updateSession(ds.session);
3945
- forkWorker(ds, '', true);
4090
+ const targetSessionId = ds.session.sessionId;
4091
+ const resumed = await withBotTurnMutation(ds.larkAppId, async () => {
4092
+ const current = [...deps.activeSessions.values()].find(candidate => candidate.session.sessionId === targetSessionId
4093
+ && candidate.session.status === 'active');
4094
+ if (!current || current !== ds)
4095
+ return { status: 'gone' };
4096
+ if (hasProtectedSessionMutationOwnership(current)) {
4097
+ return { status: 'pending', anchor: sessionAnchorId(current) };
4098
+ }
4099
+ current.workingDir = target.cwd;
4100
+ current.session.workingDir = target.cwd;
4101
+ current.session.cliSessionId = target.cliSessionId;
4102
+ current.session.title = target.title || `Import: ${project}`;
4103
+ // Resume sandbox decision is left to forkWorker (resume=true → not
4104
+ // sandboxed, matching restore semantics). Mark history so this is a resume.
4105
+ current.hasHistory = true;
4106
+ sessionStore.updateSession(current.session);
4107
+ forkWorker(current, '', true);
4108
+ return { status: 'resumed', anchor: sessionAnchorId(current) };
4109
+ });
4110
+ if (resumed.status === 'gone') {
4111
+ await sessionReply(sessionAnchorId(ds), t('cmd.no_active_session', undefined, loc));
4112
+ return;
4113
+ }
4114
+ if (resumed.status === 'pending') {
4115
+ await sessionReply(resumed.anchor, '当前 Codex App 仍有未结算消息,不能导入外部会话;请等待本轮完成或先关闭会话。');
4116
+ return;
4117
+ }
3946
4118
  const cliName = sessionCliDisplayName(ds);
3947
4119
  await sessionReply(sessionAnchorId(ds), t('cmd.adopt.resume_success', { cliName, project, title: target.title || target.cliSessionId.slice(0, 8) }, loc));
3948
4120
  }