botmux 3.4.3 → 3.5.2

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 (415) hide show
  1. package/README.en.md +10 -7
  2. package/README.md +11 -6
  3. package/dist/adapters/backend/herdr-backend.d.ts +43 -6
  4. package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
  5. package/dist/adapters/backend/herdr-backend.js +362 -28
  6. package/dist/adapters/backend/herdr-backend.js.map +1 -1
  7. package/dist/adapters/backend/reproduce-command.d.ts +39 -0
  8. package/dist/adapters/backend/reproduce-command.d.ts.map +1 -0
  9. package/dist/adapters/backend/reproduce-command.js +63 -0
  10. package/dist/adapters/backend/reproduce-command.js.map +1 -0
  11. package/dist/adapters/backend/sandbox.d.ts +99 -279
  12. package/dist/adapters/backend/sandbox.d.ts.map +1 -1
  13. package/dist/adapters/backend/sandbox.js +424 -826
  14. package/dist/adapters/backend/sandbox.js.map +1 -1
  15. package/dist/adapters/backend/session-backend-selector.d.ts +10 -1
  16. package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -1
  17. package/dist/adapters/backend/session-backend-selector.js +73 -4
  18. package/dist/adapters/backend/session-backend-selector.js.map +1 -1
  19. package/dist/adapters/backend/types.d.ts +16 -0
  20. package/dist/adapters/backend/types.d.ts.map +1 -1
  21. package/dist/adapters/backend/types.js.map +1 -1
  22. package/dist/adapters/cli/codex.d.ts.map +1 -1
  23. package/dist/adapters/cli/codex.js +4 -0
  24. package/dist/adapters/cli/codex.js.map +1 -1
  25. package/dist/adapters/cli/fs-policy.d.ts +244 -0
  26. package/dist/adapters/cli/fs-policy.d.ts.map +1 -0
  27. package/dist/adapters/cli/fs-policy.js +539 -0
  28. package/dist/adapters/cli/fs-policy.js.map +1 -0
  29. package/dist/adapters/cli/pi-initial-prompt.d.ts.map +1 -1
  30. package/dist/adapters/cli/pi-initial-prompt.js +5 -1
  31. package/dist/adapters/cli/pi-initial-prompt.js.map +1 -1
  32. package/dist/adapters/cli/read-isolation.d.ts +14 -200
  33. package/dist/adapters/cli/read-isolation.d.ts.map +1 -1
  34. package/dist/adapters/cli/read-isolation.js +18 -477
  35. package/dist/adapters/cli/read-isolation.js.map +1 -1
  36. package/dist/adapters/cli/traex.d.ts.map +1 -1
  37. package/dist/adapters/cli/traex.js +5 -0
  38. package/dist/adapters/cli/traex.js.map +1 -1
  39. package/dist/adapters/cli/types.d.ts +3 -3
  40. package/dist/adapters/cli/types.d.ts.map +1 -1
  41. package/dist/adapters/hook-installer.d.ts +7 -0
  42. package/dist/adapters/hook-installer.d.ts.map +1 -1
  43. package/dist/adapters/hook-installer.js +56 -0
  44. package/dist/adapters/hook-installer.js.map +1 -1
  45. package/dist/bot-registry.d.ts +39 -23
  46. package/dist/bot-registry.d.ts.map +1 -1
  47. package/dist/bot-registry.js +134 -2
  48. package/dist/bot-registry.js.map +1 -1
  49. package/dist/cli/capabilities.d.ts +24 -0
  50. package/dist/cli/capabilities.d.ts.map +1 -0
  51. package/dist/cli/capabilities.js +25 -0
  52. package/dist/cli/capabilities.js.map +1 -0
  53. package/dist/cli/create-group-resolver.d.ts +24 -0
  54. package/dist/cli/create-group-resolver.d.ts.map +1 -1
  55. package/dist/cli/create-group-resolver.js +23 -0
  56. package/dist/cli/create-group-resolver.js.map +1 -1
  57. package/dist/cli/daemon-lifecycle-env.d.ts +5 -7
  58. package/dist/cli/daemon-lifecycle-env.d.ts.map +1 -1
  59. package/dist/cli/daemon-lifecycle-env.js +13 -7
  60. package/dist/cli/daemon-lifecycle-env.js.map +1 -1
  61. package/dist/cli/exact-chat-grant-client.d.ts +48 -0
  62. package/dist/cli/exact-chat-grant-client.d.ts.map +1 -0
  63. package/dist/cli/exact-chat-grant-client.js +108 -0
  64. package/dist/cli/exact-chat-grant-client.js.map +1 -0
  65. package/dist/cli/exact-chat-grant.d.ts +18 -0
  66. package/dist/cli/exact-chat-grant.d.ts.map +1 -0
  67. package/dist/cli/exact-chat-grant.js +97 -0
  68. package/dist/cli/exact-chat-grant.js.map +1 -0
  69. package/dist/cli.d.ts +10 -1
  70. package/dist/cli.d.ts.map +1 -1
  71. package/dist/cli.js +795 -144
  72. package/dist/cli.js.map +1 -1
  73. package/dist/codex-rpc-engine.d.ts +26 -0
  74. package/dist/codex-rpc-engine.d.ts.map +1 -1
  75. package/dist/codex-rpc-engine.js +113 -2
  76. package/dist/codex-rpc-engine.js.map +1 -1
  77. package/dist/config.d.ts +16 -0
  78. package/dist/config.d.ts.map +1 -1
  79. package/dist/config.js +16 -0
  80. package/dist/config.js.map +1 -1
  81. package/dist/core/bot-live-control.d.ts +41 -0
  82. package/dist/core/bot-live-control.d.ts.map +1 -0
  83. package/dist/core/bot-live-control.js +135 -0
  84. package/dist/core/bot-live-control.js.map +1 -0
  85. package/dist/core/command-handler.d.ts.map +1 -1
  86. package/dist/core/command-handler.js +44 -66
  87. package/dist/core/command-handler.js.map +1 -1
  88. package/dist/core/daemon-config-fence.d.ts +8 -0
  89. package/dist/core/daemon-config-fence.d.ts.map +1 -0
  90. package/dist/core/daemon-config-fence.js +13 -0
  91. package/dist/core/daemon-config-fence.js.map +1 -0
  92. package/dist/core/dashboard-events.d.ts +5 -0
  93. package/dist/core/dashboard-events.d.ts.map +1 -1
  94. package/dist/core/dashboard-events.js.map +1 -1
  95. package/dist/core/dashboard-ipc-server.d.ts +21 -2
  96. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  97. package/dist/core/dashboard-ipc-server.js +258 -32
  98. package/dist/core/dashboard-ipc-server.js.map +1 -1
  99. package/dist/core/dispatch-registry.d.ts +12 -0
  100. package/dist/core/dispatch-registry.d.ts.map +1 -0
  101. package/dist/core/dispatch-registry.js +33 -0
  102. package/dist/core/dispatch-registry.js.map +1 -0
  103. package/dist/core/dispatch.d.ts +99 -0
  104. package/dist/core/dispatch.d.ts.map +1 -1
  105. package/dist/core/dispatch.js +136 -0
  106. package/dist/core/dispatch.js.map +1 -1
  107. package/dist/core/maintenance.d.ts.map +1 -1
  108. package/dist/core/maintenance.js +10 -3
  109. package/dist/core/maintenance.js.map +1 -1
  110. package/dist/core/passthrough-commands.d.ts.map +1 -1
  111. package/dist/core/passthrough-commands.js +1 -1
  112. package/dist/core/passthrough-commands.js.map +1 -1
  113. package/dist/core/persistent-backend.d.ts +16 -2
  114. package/dist/core/persistent-backend.d.ts.map +1 -1
  115. package/dist/core/persistent-backend.js +40 -0
  116. package/dist/core/persistent-backend.js.map +1 -1
  117. package/dist/core/plugins/mcp/gateway-installer.d.ts.map +1 -1
  118. package/dist/core/plugins/mcp/gateway-installer.js +14 -2
  119. package/dist/core/plugins/mcp/gateway-installer.js.map +1 -1
  120. package/dist/core/session-discovery.d.ts +8 -1
  121. package/dist/core/session-discovery.d.ts.map +1 -1
  122. package/dist/core/session-discovery.js +140 -8
  123. package/dist/core/session-discovery.js.map +1 -1
  124. package/dist/core/session-manager.d.ts.map +1 -1
  125. package/dist/core/session-manager.js +102 -68
  126. package/dist/core/session-manager.js.map +1 -1
  127. package/dist/core/session-title.d.ts +8 -0
  128. package/dist/core/session-title.d.ts.map +1 -1
  129. package/dist/core/session-title.js +54 -0
  130. package/dist/core/session-title.js.map +1 -1
  131. package/dist/core/terminal-write-auth.d.ts +15 -0
  132. package/dist/core/terminal-write-auth.d.ts.map +1 -1
  133. package/dist/core/terminal-write-auth.js +20 -0
  134. package/dist/core/terminal-write-auth.js.map +1 -1
  135. package/dist/core/trigger-session.d.ts.map +1 -1
  136. package/dist/core/trigger-session.js +3 -2
  137. package/dist/core/trigger-session.js.map +1 -1
  138. package/dist/core/types.d.ts +33 -0
  139. package/dist/core/types.d.ts.map +1 -1
  140. package/dist/core/types.js.map +1 -1
  141. package/dist/core/worker-pool.d.ts +51 -8
  142. package/dist/core/worker-pool.d.ts.map +1 -1
  143. package/dist/core/worker-pool.js +439 -25
  144. package/dist/core/worker-pool.js.map +1 -1
  145. package/dist/daemon.d.ts +4 -2
  146. package/dist/daemon.d.ts.map +1 -1
  147. package/dist/daemon.js +403 -26
  148. package/dist/daemon.js.map +1 -1
  149. package/dist/dashboard/bot-onboarding.d.ts +127 -1
  150. package/dist/dashboard/bot-onboarding.d.ts.map +1 -1
  151. package/dist/dashboard/bot-onboarding.js +1104 -25
  152. package/dist/dashboard/bot-onboarding.js.map +1 -1
  153. package/dist/dashboard/bot-payload.d.ts +5 -0
  154. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  155. package/dist/dashboard/bot-payload.js +7 -0
  156. package/dist/dashboard/bot-payload.js.map +1 -1
  157. package/dist/dashboard/debug-terminal.d.ts +18 -0
  158. package/dist/dashboard/debug-terminal.d.ts.map +1 -0
  159. package/dist/dashboard/debug-terminal.js +338 -0
  160. package/dist/dashboard/debug-terminal.js.map +1 -0
  161. package/dist/dashboard/public-redact.d.ts +2 -0
  162. package/dist/dashboard/public-redact.d.ts.map +1 -1
  163. package/dist/dashboard/public-redact.js +12 -3
  164. package/dist/dashboard/public-redact.js.map +1 -1
  165. package/dist/dashboard/registry.d.ts +8 -0
  166. package/dist/dashboard/registry.d.ts.map +1 -1
  167. package/dist/dashboard/registry.js +13 -0
  168. package/dist/dashboard/registry.js.map +1 -1
  169. package/dist/dashboard/settings-write-applier.d.ts +47 -1
  170. package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
  171. package/dist/dashboard/settings-write-applier.js +102 -1
  172. package/dist/dashboard/settings-write-applier.js.map +1 -1
  173. package/dist/dashboard/web/app.d.ts.map +1 -1
  174. package/dist/dashboard/web/app.js +2 -0
  175. package/dist/dashboard/web/app.js.map +1 -1
  176. package/dist/dashboard/web/bot-defaults-page.d.ts +17 -0
  177. package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
  178. package/dist/dashboard/web/bot-defaults-page.js +249 -11
  179. package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
  180. package/dist/dashboard/web/bot-defaults.d.ts +22 -0
  181. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  182. package/dist/dashboard/web/bot-defaults.js +19 -0
  183. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  184. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  185. package/dist/dashboard/web/i18n.js +114 -28
  186. package/dist/dashboard/web/i18n.js.map +1 -1
  187. package/dist/dashboard/web/icon-tooltip.d.ts +3 -0
  188. package/dist/dashboard/web/icon-tooltip.d.ts.map +1 -0
  189. package/dist/dashboard/web/icon-tooltip.js +108 -0
  190. package/dist/dashboard/web/icon-tooltip.js.map +1 -0
  191. package/dist/dashboard/web/sessions-kanban.js +2 -2
  192. package/dist/dashboard/web/sessions-kanban.js.map +1 -1
  193. package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
  194. package/dist/dashboard/web/sessions-page.js +49 -53
  195. package/dist/dashboard/web/sessions-page.js.map +1 -1
  196. package/dist/dashboard/web/sessions.d.ts +2 -0
  197. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  198. package/dist/dashboard/web/sessions.js +46 -2
  199. package/dist/dashboard/web/sessions.js.map +1 -1
  200. package/dist/dashboard/web/settings-page.d.ts +78 -0
  201. package/dist/dashboard/web/settings-page.d.ts.map +1 -1
  202. package/dist/dashboard/web/settings-page.js +88 -1
  203. package/dist/dashboard/web/settings-page.js.map +1 -1
  204. package/dist/dashboard/web/store.d.ts +2 -0
  205. package/dist/dashboard/web/store.d.ts.map +1 -1
  206. package/dist/dashboard/web/store.js +13 -1
  207. package/dist/dashboard/web/store.js.map +1 -1
  208. package/dist/dashboard-web/app.js +1 -1
  209. package/dist/dashboard-web/chunks/bot-defaults-page-AUKV7P4L.js +10 -0
  210. package/dist/dashboard-web/chunks/{chunk-P6GO5KT4.js → chunk-KPQAUX7V.js} +1 -1
  211. package/dist/dashboard-web/chunks/{chunk-UBOLHQCK.js → chunk-NRL6CVAS.js} +1 -1
  212. package/dist/dashboard-web/chunks/{chunk-JNRSPKLS.js → chunk-R7IAKAYX.js} +1 -1
  213. package/dist/dashboard-web/chunks/{chunk-CUV65BIQ.js → chunk-X2L65IOM.js} +8 -8
  214. package/dist/dashboard-web/chunks/{connectors-page-KTC2QWNW.js → connectors-page-HDQFEXZI.js} +1 -1
  215. package/dist/dashboard-web/chunks/{groups-page-B3EKGOLY.js → groups-page-6DKXBYOM.js} +1 -1
  216. package/dist/dashboard-web/chunks/{insights-page-X6RXNCWX.js → insights-page-NWARTJ6J.js} +1 -1
  217. package/dist/dashboard-web/chunks/{monitor-room-LLRA5LQW.js → monitor-room-AGX3XRJX.js} +1 -1
  218. package/dist/dashboard-web/chunks/{monitoring-page-7MT5FEOS.js → monitoring-page-GGGIWN4C.js} +1 -1
  219. package/dist/dashboard-web/chunks/{office-page-KD46D5TD.js → office-page-6SIVKSK3.js} +1 -1
  220. package/dist/dashboard-web/chunks/{overview-page-JUWNH32O.js → overview-page-FVR3NKAF.js} +1 -1
  221. package/dist/dashboard-web/chunks/{roles-page-P6F7NLOI.js → roles-page-WE2YB7KN.js} +1 -1
  222. package/dist/dashboard-web/chunks/{schedules-page-XIHWZST4.js → schedules-page-TB3I24XN.js} +1 -1
  223. package/dist/dashboard-web/chunks/sessions-page-KOOOASIL.js +2 -0
  224. package/dist/dashboard-web/chunks/settings-page-IEBR7TGN.js +2 -0
  225. package/dist/dashboard-web/chunks/{skills-page-A46KVWBL.js → skills-page-3QAT2IWD.js} +1 -1
  226. package/dist/dashboard-web/chunks/{team-federation-page-SDUC56DF.js → team-federation-page-K4VVAVAK.js} +1 -1
  227. package/dist/dashboard-web/chunks/{v3-page-J5JOTUSQ.js → v3-page-V6IOJR2B.js} +1 -1
  228. package/dist/dashboard-web/chunks/{whiteboards-page-U7LSOAWI.js → whiteboards-page-V6CGKRNX.js} +1 -1
  229. package/dist/dashboard-web/style.css +218 -10
  230. package/dist/dashboard.js +380 -8
  231. package/dist/dashboard.js.map +1 -1
  232. package/dist/features/codex-notifier/adoption.d.ts +11 -0
  233. package/dist/features/codex-notifier/adoption.d.ts.map +1 -0
  234. package/dist/features/codex-notifier/adoption.js +11 -0
  235. package/dist/features/codex-notifier/adoption.js.map +1 -0
  236. package/dist/features/codex-notifier/app-opener.d.ts +21 -0
  237. package/dist/features/codex-notifier/app-opener.d.ts.map +1 -0
  238. package/dist/features/codex-notifier/app-opener.js +33 -0
  239. package/dist/features/codex-notifier/app-opener.js.map +1 -0
  240. package/dist/features/codex-notifier/card-action.d.ts +27 -0
  241. package/dist/features/codex-notifier/card-action.d.ts.map +1 -0
  242. package/dist/features/codex-notifier/card-action.js +84 -0
  243. package/dist/features/codex-notifier/card-action.js.map +1 -0
  244. package/dist/features/codex-notifier/card.d.ts +12 -0
  245. package/dist/features/codex-notifier/card.d.ts.map +1 -0
  246. package/dist/features/codex-notifier/card.js +150 -0
  247. package/dist/features/codex-notifier/card.js.map +1 -0
  248. package/dist/features/codex-notifier/codex-context.d.ts +12 -0
  249. package/dist/features/codex-notifier/codex-context.d.ts.map +1 -0
  250. package/dist/features/codex-notifier/codex-context.js +161 -0
  251. package/dist/features/codex-notifier/codex-context.js.map +1 -0
  252. package/dist/features/codex-notifier/config.d.ts +9 -0
  253. package/dist/features/codex-notifier/config.d.ts.map +1 -0
  254. package/dist/features/codex-notifier/config.js +11 -0
  255. package/dist/features/codex-notifier/config.js.map +1 -0
  256. package/dist/features/codex-notifier/confirmed-turn.d.ts +20 -0
  257. package/dist/features/codex-notifier/confirmed-turn.d.ts.map +1 -0
  258. package/dist/features/codex-notifier/confirmed-turn.js +130 -0
  259. package/dist/features/codex-notifier/confirmed-turn.js.map +1 -0
  260. package/dist/features/codex-notifier/emitter.d.ts +13 -0
  261. package/dist/features/codex-notifier/emitter.d.ts.map +1 -0
  262. package/dist/features/codex-notifier/emitter.js +33 -0
  263. package/dist/features/codex-notifier/emitter.js.map +1 -0
  264. package/dist/features/codex-notifier/event-store.d.ts +29 -0
  265. package/dist/features/codex-notifier/event-store.d.ts.map +1 -0
  266. package/dist/features/codex-notifier/event-store.js +217 -0
  267. package/dist/features/codex-notifier/event-store.js.map +1 -0
  268. package/dist/features/codex-notifier/event.d.ts +46 -0
  269. package/dist/features/codex-notifier/event.d.ts.map +1 -0
  270. package/dist/features/codex-notifier/event.js +188 -0
  271. package/dist/features/codex-notifier/event.js.map +1 -0
  272. package/dist/features/codex-notifier/hook-cli.d.ts +23 -0
  273. package/dist/features/codex-notifier/hook-cli.d.ts.map +1 -0
  274. package/dist/features/codex-notifier/hook-cli.js +130 -0
  275. package/dist/features/codex-notifier/hook-cli.js.map +1 -0
  276. package/dist/features/codex-notifier/hook-installer.d.ts +19 -0
  277. package/dist/features/codex-notifier/hook-installer.d.ts.map +1 -0
  278. package/dist/features/codex-notifier/hook-installer.js +158 -0
  279. package/dist/features/codex-notifier/hook-installer.js.map +1 -0
  280. package/dist/features/codex-notifier/index.d.ts +21 -0
  281. package/dist/features/codex-notifier/index.d.ts.map +1 -0
  282. package/dist/features/codex-notifier/index.js +19 -0
  283. package/dist/features/codex-notifier/index.js.map +1 -0
  284. package/dist/features/codex-notifier/internal-turn.d.ts +5 -0
  285. package/dist/features/codex-notifier/internal-turn.d.ts.map +1 -0
  286. package/dist/features/codex-notifier/internal-turn.js +43 -0
  287. package/dist/features/codex-notifier/internal-turn.js.map +1 -0
  288. package/dist/features/codex-notifier/outbox-worker.d.ts +71 -0
  289. package/dist/features/codex-notifier/outbox-worker.d.ts.map +1 -0
  290. package/dist/features/codex-notifier/outbox-worker.js +290 -0
  291. package/dist/features/codex-notifier/outbox-worker.js.map +1 -0
  292. package/dist/features/codex-notifier/outbox.d.ts +25 -0
  293. package/dist/features/codex-notifier/outbox.d.ts.map +1 -0
  294. package/dist/features/codex-notifier/outbox.js +225 -0
  295. package/dist/features/codex-notifier/outbox.js.map +1 -0
  296. package/dist/features/codex-notifier/paths.d.ts +7 -0
  297. package/dist/features/codex-notifier/paths.d.ts.map +1 -0
  298. package/dist/features/codex-notifier/paths.js +20 -0
  299. package/dist/features/codex-notifier/paths.js.map +1 -0
  300. package/dist/features/codex-notifier/screen-lock.d.ts +12 -0
  301. package/dist/features/codex-notifier/screen-lock.d.ts.map +1 -0
  302. package/dist/features/codex-notifier/screen-lock.js +47 -0
  303. package/dist/features/codex-notifier/screen-lock.js.map +1 -0
  304. package/dist/features/codex-notifier/side-conversation-monitor.d.ts +102 -0
  305. package/dist/features/codex-notifier/side-conversation-monitor.d.ts.map +1 -0
  306. package/dist/features/codex-notifier/side-conversation-monitor.js +774 -0
  307. package/dist/features/codex-notifier/side-conversation-monitor.js.map +1 -0
  308. package/dist/features/codex-notifier/types.d.ts +42 -0
  309. package/dist/features/codex-notifier/types.d.ts.map +1 -0
  310. package/dist/features/codex-notifier/types.js +2 -0
  311. package/dist/features/codex-notifier/types.js.map +1 -0
  312. package/dist/features/codex-notifier/worker-lock.d.ts +8 -0
  313. package/dist/features/codex-notifier/worker-lock.d.ts.map +1 -0
  314. package/dist/features/codex-notifier/worker-lock.js +108 -0
  315. package/dist/features/codex-notifier/worker-lock.js.map +1 -0
  316. package/dist/global-config.d.ts +13 -0
  317. package/dist/global-config.d.ts.map +1 -1
  318. package/dist/global-config.js +34 -1
  319. package/dist/global-config.js.map +1 -1
  320. package/dist/i18n/en.d.ts.map +1 -1
  321. package/dist/i18n/en.js +1 -19
  322. package/dist/i18n/en.js.map +1 -1
  323. package/dist/i18n/zh.d.ts.map +1 -1
  324. package/dist/i18n/zh.js +1 -19
  325. package/dist/i18n/zh.js.map +1 -1
  326. package/dist/im/lark/card-builder.d.ts +0 -13
  327. package/dist/im/lark/card-builder.d.ts.map +1 -1
  328. package/dist/im/lark/card-builder.js +2 -36
  329. package/dist/im/lark/card-builder.js.map +1 -1
  330. package/dist/im/lark/card-handler.d.ts +2 -0
  331. package/dist/im/lark/card-handler.d.ts.map +1 -1
  332. package/dist/im/lark/card-handler.js +166 -64
  333. package/dist/im/lark/card-handler.js.map +1 -1
  334. package/dist/im/lark/client.d.ts +98 -4
  335. package/dist/im/lark/client.d.ts.map +1 -1
  336. package/dist/im/lark/client.js +317 -29
  337. package/dist/im/lark/client.js.map +1 -1
  338. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  339. package/dist/im/lark/event-dispatcher.js +48 -33
  340. package/dist/im/lark/event-dispatcher.js.map +1 -1
  341. package/dist/services/codex-app-threads.d.ts +47 -0
  342. package/dist/services/codex-app-threads.d.ts.map +1 -1
  343. package/dist/services/codex-app-threads.js +502 -10
  344. package/dist/services/codex-app-threads.js.map +1 -1
  345. package/dist/services/exact-chat-grant.d.ts +83 -0
  346. package/dist/services/exact-chat-grant.d.ts.map +1 -0
  347. package/dist/services/exact-chat-grant.js +249 -0
  348. package/dist/services/exact-chat-grant.js.map +1 -0
  349. package/dist/services/group-creator.d.ts +11 -0
  350. package/dist/services/group-creator.d.ts.map +1 -1
  351. package/dist/services/group-creator.js +18 -9
  352. package/dist/services/group-creator.js.map +1 -1
  353. package/dist/services/local-cli-opener.d.ts.map +1 -1
  354. package/dist/services/local-cli-opener.js +10 -5
  355. package/dist/services/local-cli-opener.js.map +1 -1
  356. package/dist/services/sandbox-migration.d.ts +14 -0
  357. package/dist/services/sandbox-migration.d.ts.map +1 -0
  358. package/dist/services/sandbox-migration.js +85 -0
  359. package/dist/services/sandbox-migration.js.map +1 -0
  360. package/dist/services/sandbox-store.d.ts +28 -0
  361. package/dist/services/sandbox-store.d.ts.map +1 -1
  362. package/dist/services/sandbox-store.js +82 -0
  363. package/dist/services/sandbox-store.js.map +1 -1
  364. package/dist/services/traex-user-input.d.ts +19 -0
  365. package/dist/services/traex-user-input.d.ts.map +1 -0
  366. package/dist/services/traex-user-input.js +57 -0
  367. package/dist/services/traex-user-input.js.map +1 -0
  368. package/dist/setup/open-platform-automation.d.ts +24 -3
  369. package/dist/setup/open-platform-automation.d.ts.map +1 -1
  370. package/dist/setup/open-platform-automation.js +45 -8
  371. package/dist/setup/open-platform-automation.js.map +1 -1
  372. package/dist/setup/verify-permissions.d.ts +18 -0
  373. package/dist/setup/verify-permissions.d.ts.map +1 -1
  374. package/dist/setup/verify-permissions.js +60 -0
  375. package/dist/setup/verify-permissions.js.map +1 -1
  376. package/dist/types.d.ts +83 -5
  377. package/dist/types.d.ts.map +1 -1
  378. package/dist/utils/child-env.d.ts +2 -2
  379. package/dist/utils/child-env.d.ts.map +1 -1
  380. package/dist/utils/child-env.js +14 -0
  381. package/dist/utils/child-env.js.map +1 -1
  382. package/dist/utils/stuck-detector.d.ts +45 -0
  383. package/dist/utils/stuck-detector.d.ts.map +1 -0
  384. package/dist/utils/stuck-detector.js +188 -0
  385. package/dist/utils/stuck-detector.js.map +1 -0
  386. package/dist/utils/stuck-key-guard.d.ts +75 -0
  387. package/dist/utils/stuck-key-guard.d.ts.map +1 -0
  388. package/dist/utils/stuck-key-guard.js +77 -0
  389. package/dist/utils/stuck-key-guard.js.map +1 -0
  390. package/dist/worker.js +1119 -588
  391. package/dist/worker.js.map +1 -1
  392. package/dist/workflows/events/payloads.d.ts +56 -0
  393. package/dist/workflows/events/payloads.d.ts.map +1 -1
  394. package/dist/workflows/events/payloads.js +9 -0
  395. package/dist/workflows/events/payloads.js.map +1 -1
  396. package/dist/workflows/events/schema.d.ts +86 -0
  397. package/dist/workflows/events/schema.d.ts.map +1 -1
  398. package/dist/workflows/shared/sandbox-policy.d.ts +10 -1
  399. package/dist/workflows/shared/sandbox-policy.d.ts.map +1 -1
  400. package/dist/workflows/shared/sandbox-policy.js +12 -0
  401. package/dist/workflows/shared/sandbox-policy.js.map +1 -1
  402. package/dist/workflows/v3/bot-resolve.d.ts.map +1 -1
  403. package/dist/workflows/v3/bot-resolve.js +36 -0
  404. package/dist/workflows/v3/bot-resolve.js.map +1 -1
  405. package/dist/workflows/v3/contract.d.ts +8 -0
  406. package/dist/workflows/v3/contract.d.ts.map +1 -1
  407. package/dist/workflows/v3/contract.js.map +1 -1
  408. package/package.json +1 -1
  409. package/dist/dashboard-web/chunks/bot-defaults-page-B5AY4B2J.js +0 -5
  410. package/dist/dashboard-web/chunks/sessions-page-2VECQ27T.js +0 -3
  411. package/dist/dashboard-web/chunks/settings-page-HMIUE2JA.js +0 -2
  412. package/dist/services/sandbox-land.d.ts +0 -45
  413. package/dist/services/sandbox-land.d.ts.map +0 -1
  414. package/dist/services/sandbox-land.js +0 -394
  415. package/dist/services/sandbox-land.js.map +0 -1
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url';
11
11
  import { ensureSkills, ensureAskSkill, ensurePluginSkills, ensureWhiteboardSkill, removeGlobalBotmuxSkills } from '../skills/installer.js';
12
12
  import { shouldInstallGlobalSkills } from '../skills/injection-mode.js';
13
13
  import { whiteboardEnabled } from '../services/whiteboard-store.js';
14
- import { installHook } from '../adapters/hook-installer.js';
14
+ import { cleanupTraexAskHooks, installHook } from '../adapters/hook-installer.js';
15
15
  import { hookCommandFor } from '../adapters/hook-command.js';
16
16
  import { randomBytes, randomUUID } from 'node:crypto';
17
17
  import { config } from '../config.js';
@@ -24,6 +24,7 @@ import { loadFrozenCards, saveFrozenCards } from '../services/frozen-card-store.
24
24
  import { hashUrlForLog, cancelRiffTaskById } from '../adapters/backend/riff-backend.js';
25
25
  import { logger } from '../utils/logger.js';
26
26
  import { createCliAdapterSync } from '../adapters/cli/registry.js';
27
+ import { traeHome } from '../services/traex-paths.js';
27
28
  import { botLocale, localeForBot, t as tr } from '../i18n/index.js';
28
29
  import { claudeJsonlPathForSession } from '../adapters/cli/claude-code.js';
29
30
  import { findUniqueClaudeSessionByCwd } from './session-discovery.js';
@@ -34,7 +35,7 @@ import { listDocSubscriptionsForSession, removeDocSubscription } from '../servic
34
35
  import { TmuxBackend } from '../adapters/backend/tmux-backend.js';
35
36
  import { HerdrBackend } from '../adapters/backend/herdr-backend.js';
36
37
  import { sandboxEnabled } from '../adapters/backend/sandbox.js';
37
- import { isSuspendableBackendType, getSessionPersistentBackendType, persistentSessionName, killPersistentSession, resolvePairedSpawnBackendType } from './persistent-backend.js';
38
+ import { isSuspendableBackendType, getSessionPersistentBackendType, persistentBackendTargetForSession, killPersistentBackendTarget, managedTargetsForCliChange, resolvePairedSpawnBackendType, resolvePersistentBackendTarget } from './persistent-backend.js';
38
39
  import { getBot, getAllBots, resolveBrandLabel } from '../bot-registry.js';
39
40
  /** A random id minted once per daemon process (this lifetime). Stamped onto
40
41
  * isolated persistent panes so a suspend→resume reattach (same id) is
@@ -79,7 +80,9 @@ import { isLocalCliOpenEnabled, isLocalCliOpenReady } from '../services/local-cl
79
80
  import { isSilentScheduledTurn } from './silent-schedule-turns.js';
80
81
  import { writeDeferredTopicBinding } from './deferred-topic-binding.js';
81
82
  import { deferWorkerSpawnDuringDeviceIsolation } from './device-isolation-activation.js';
83
+ import { buildBotmuxLarkNativeSessionTitle, extractBotmuxLarkNativeSessionTitlePrompt, } from './session-title.js';
82
84
  import { acknowledgeSessionReady } from './session-ready-handshake.js';
85
+ import { recordDispatchInputCommit } from './dispatch.js';
83
86
  const __filename = fileURLToPath(import.meta.url);
84
87
  const __dirname = dirname(__filename);
85
88
  const WORKER_SIGTERM_BACKSTOP_MS = 2_000;
@@ -652,13 +655,13 @@ export async function postPrivateSnapshotCard(ds, audience) {
652
655
  * Deliver the write-enabled session card (the "🔑 获取操作链接" card, which carries
653
656
  * a write-token terminal URL + manage buttons) privately to a single operator.
654
657
  *
655
- * Prefers an in-chat "visible-to-you" ephemeral card so the operator never has
656
- * to leave the conversation. Feishu's ephemeral API only works in plain `group`
657
- * chats topic / thread groups reject with {@link LARK_CODE_EPHEMERAL_NOT_GROUP}
658
- * (18053) and p2p chats are unsupported and chatType can't distinguish a topic
659
- * group from a regular one (both record 'group'), so we attempt ephemeral for any
660
- * non-p2p chat and fall back to a private DM on ANY failure. p2p chats skip the
661
- * doomed ephemeral attempt and DM directly (the DM lands in that same 1:1 chat).
658
+ * Prefers an in-chat "visible-to-you" ephemeral card in a flat group so the
659
+ * operator never has to leave the conversation. Thread-scope sessions and
660
+ * chat-scope sessions currently folded into a thread go straight to DM:
661
+ * Feishu may accept their ephemeral message without rendering it in the topic
662
+ * panel. p2p chats also DM directly (the DM lands in that same 1:1 chat).
663
+ *
664
+ * Other group chats attempt ephemeral first and fall back to DM on ANY failure.
662
665
  *
663
666
  * Both channels are private, so the DM fallback never leaks the write token —
664
667
  * unlike the private /card snapshot (which fails closed), here we fail OVER.
@@ -667,7 +670,10 @@ export async function postPrivateSnapshotCard(ds, audience) {
667
670
  */
668
671
  export async function deliverWriteLinkCard(ds, operatorOpenId, cardJson) {
669
672
  const who = operatorOpenId.substring(0, 8);
670
- if (ds.chatType !== 'p2p') {
673
+ const replyTarget = ds.currentReplyTarget ?? ds.session.currentReplyTarget;
674
+ const isThreaded = ds.scope === 'thread'
675
+ || (!!replyTarget?.rootMessageId && replyTarget.quoteOnly !== true);
676
+ if (ds.chatType !== 'p2p' && !isThreaded) {
671
677
  try {
672
678
  await sendEphemeralCard(ds.larkAppId, ds.chatId, operatorOpenId, cardJson);
673
679
  logger.info(`[${tag(ds)}] write link delivered via ephemeral card to ${who}…`);
@@ -909,6 +915,12 @@ const skillsInstalledCliIds = new Set();
909
915
  */
910
916
  export function ensureCliSkills(cliId, cliPathOverride) {
911
917
  const adapter = createCliAdapterSync(cliId, cliPathOverride);
918
+ if (cliId === 'traex') {
919
+ cleanupTraexAskHooks([
920
+ join(traeHome(), 'hooks.json'),
921
+ join(traeHome(), 'cli', 'hooks.json'),
922
+ ]);
923
+ }
912
924
  // Claude-family CLIs deliver skills per-session via `--plugin-dir` (no global
913
925
  // leak), so they always materialise their plugin dir — the builtin-injection
914
926
  // mode does not apply to them. Everything below the branch is the global-dir
@@ -1084,6 +1096,10 @@ export function killWorker(ds) {
1084
1096
  // daemon-side copy synchronously; the worker may never get a chance to send
1085
1097
  // its ordered revoke IPC on close/crash paths.
1086
1098
  ds.managedTurnOrigin = undefined;
1099
+ // The worker/CLI generation is ending — any outstanding stuck-warning card
1100
+ // must be invalidated so a late click cannot inject keys into a replacement
1101
+ // worker (or into nothing, if no replacement comes).
1102
+ invalidateStuckWarning(ds, 'killWorker');
1087
1103
  if (!ds.worker || ds.worker.killed) {
1088
1104
  // No live worker to receive {type:'close'}, so its destroySession() — which
1089
1105
  // tears down the persistent backing session (tmux/herdr/zellij) — never
@@ -1149,7 +1165,7 @@ function destroyOrphanedBackingSession(ds) {
1149
1165
  if (!backendType)
1150
1166
  return;
1151
1167
  try {
1152
- killPersistentSession(backendType, persistentSessionName(backendType, ds.session.sessionId));
1168
+ killPersistentBackendTarget(persistentBackendTargetForSession(ds));
1153
1169
  logger.info(`[${tag(ds)}] killWorker: no live worker — destroyed orphaned ${backendType} backing session`);
1154
1170
  }
1155
1171
  catch (err) {
@@ -1201,6 +1217,10 @@ export function suspendWorker(ds, reason = 'suspended_idle') {
1201
1217
  ds.workerToken = null;
1202
1218
  ds.workerViewToken = null;
1203
1219
  ds.managedTurnOrigin = undefined;
1220
+ // The worker is being suspended — the CLI will be destroyed and cold-resumed
1221
+ // later. Invalidate any stuck-warning card so a late click cannot inject keys
1222
+ // after the CLI is gone (or into a different CLI on resume).
1223
+ invalidateStuckWarning(ds, 'suspendWorker');
1204
1224
  // Screen state describes the process we just stopped. Keeping it would make
1205
1225
  // the dashboard hydrate this process-less logical session as idle/working.
1206
1226
  ds.lastScreenStatus = undefined;
@@ -1617,11 +1637,32 @@ export function sendWorkerInput(ds, payload, turnId, opts = {}) {
1617
1637
  return false;
1618
1638
  const normalized = typeof payload === 'string' ? { content: payload } : payload;
1619
1639
  const codexAppInput = codexAppInputForSession(ds, normalized.codexAppInput, turnId);
1640
+ let nativeSessionTitlePrompt;
1641
+ let nativeSessionTitle;
1642
+ if (ds.session.nativeSessionTitleAwaitingContent && !ds.session.nativeSessionTitleUserDefined && !ds.adoptedFrom) {
1643
+ const bot = getBot(ds.larkAppId);
1644
+ const effectiveCliId = ds.session.cliId ?? bot.config.cliId;
1645
+ if (effectiveCliId === 'codex') {
1646
+ nativeSessionTitlePrompt = extractBotmuxLarkNativeSessionTitlePrompt(normalized.codexAppInput?.text ?? normalized.content, bot.botName ? [{ name: bot.botName }] : undefined);
1647
+ if (nativeSessionTitlePrompt) {
1648
+ nativeSessionTitle = buildBotmuxLarkNativeSessionTitle(nativeSessionTitlePrompt);
1649
+ ds.session.nativeSessionTitle = nativeSessionTitle;
1650
+ ds.session.nativeSessionTitleAwaitingContent = undefined;
1651
+ if (ds.initConfig) {
1652
+ ds.initConfig.nativeSessionTitle = nativeSessionTitle;
1653
+ ds.initConfig.nativeSessionTitlePrompt = nativeSessionTitlePrompt;
1654
+ }
1655
+ sessionStore.updateSession(ds.session);
1656
+ }
1657
+ }
1658
+ }
1620
1659
  const vcMeetingImTurnOrigin = resolveVcMeetingImTurnOrigin(ds.session, turnId);
1621
1660
  ds.worker.send({
1622
1661
  type: 'message',
1623
1662
  content: normalized.content,
1624
1663
  ...(codexAppInput ? { codexAppInput } : {}),
1664
+ ...(nativeSessionTitle ? { nativeSessionTitle } : {}),
1665
+ ...(nativeSessionTitlePrompt ? { nativeSessionTitlePrompt } : {}),
1625
1666
  ...(turnId ? { turnId } : {}),
1626
1667
  ...(opts.dispatchAttempt !== undefined ? { dispatchAttempt: opts.dispatchAttempt } : {}),
1627
1668
  ...(vcMeetingImTurnOrigin
@@ -1725,6 +1766,7 @@ export function forkWorker(ds, promptInput, resumeOrTurnId = false) {
1725
1766
  if (ds.session.sandbox === undefined) {
1726
1767
  if (!resume) {
1727
1768
  ds.session.sandbox = botCfg.sandbox === true;
1769
+ ds.session.sandboxPaths = botCfg.sandboxPaths;
1728
1770
  ds.session.sandboxHidePaths = botCfg.sandboxHidePaths ?? [];
1729
1771
  ds.session.sandboxReadonlyPaths = botCfg.sandboxReadonlyPaths ?? [];
1730
1772
  ds.session.sandboxNetwork = botCfg.sandboxNetwork !== false;
@@ -1737,6 +1779,10 @@ export function forkWorker(ds, promptInput, resumeOrTurnId = false) {
1737
1779
  }
1738
1780
  sessionStore.updateSession(ds.session);
1739
1781
  }
1782
+ // Reserve and durably publish the replacement lifetime before killing an
1783
+ // existing worker. A failed reservation leaves the old worker untouched;
1784
+ // a successful reservation immediately invalidates any late old-worker ACK.
1785
+ const workerGeneration = reserveWorkerGeneration(ds);
1740
1786
  // Guard against double-fork: if a worker is already running, kill it first
1741
1787
  if (ds.worker && !ds.worker.killed) {
1742
1788
  logger.warn(`[${t}] Worker already running (pid: ${ds.worker.pid}), killing before re-fork`);
@@ -1769,6 +1815,38 @@ export function forkWorker(ds, promptInput, resumeOrTurnId = false) {
1769
1815
  reclaimParkedCrashDiagnostic(ds);
1770
1816
  const agentCfg = sessionAgentConfig(ds, botCfg);
1771
1817
  ensureCliEnv(agentCfg.cliId, agentCfg.cliPathOverride);
1818
+ let nativeSessionTitle;
1819
+ let nativeSessionTitlePrompt;
1820
+ if (agentCfg.cliId === 'codex' && !ds.adoptedFrom) {
1821
+ const isFreshNativeSession = !resume && !ds.session.cliSessionId;
1822
+ const titlePrompt = extractBotmuxLarkNativeSessionTitlePrompt(promptPayload.codexAppInput?.text ?? prompt, bot.botName ? [{ name: bot.botName }] : undefined);
1823
+ if (isFreshNativeSession && !ds.session.nativeSessionTitleUserDefined) {
1824
+ ds.session.nativeSessionTitle = buildBotmuxLarkNativeSessionTitle(titlePrompt ? ds.session.title : undefined, bot.botName ? [{ name: bot.botName }] : undefined, ds.chatType === 'group' ? ds.session.chatDisplayName : undefined);
1825
+ ds.session.nativeSessionTitleAwaitingContent = titlePrompt ? undefined : true;
1826
+ nativeSessionTitlePrompt = titlePrompt;
1827
+ sessionStore.updateSession(ds.session);
1828
+ }
1829
+ else if (ds.session.nativeSessionTitleAwaitingContent
1830
+ && !ds.session.nativeSessionTitleUserDefined
1831
+ && titlePrompt) {
1832
+ ds.session.nativeSessionTitle = buildBotmuxLarkNativeSessionTitle(titlePrompt);
1833
+ ds.session.nativeSessionTitleAwaitingContent = undefined;
1834
+ nativeSessionTitlePrompt = titlePrompt;
1835
+ sessionStore.updateSession(ds.session);
1836
+ }
1837
+ else if (isFreshNativeSession && !ds.session.nativeSessionTitle) {
1838
+ ds.session.nativeSessionTitle = ds.session.title;
1839
+ sessionStore.updateSession(ds.session);
1840
+ }
1841
+ if (isFreshNativeSession
1842
+ || (resume && !!ds.session.cliSessionId && !!ds.session.nativeSessionTitle)
1843
+ || (resume
1844
+ && !!ds.session.nativeSessionTitleAwaitingContent
1845
+ && !!ds.session.nativeSessionTitle)
1846
+ || (!!nativeSessionTitlePrompt && !!ds.session.nativeSessionTitle)) {
1847
+ nativeSessionTitle = ds.session.nativeSessionTitle;
1848
+ }
1849
+ }
1772
1850
  // Claude Code blocks on the interactive folder-trust dialog the first time
1773
1851
  // it runs in an untrusted workingDir; pre-accept it so the spawn doesn't hang.
1774
1852
  // Seed CLI (Claude Code fork) has the same dialog — drive both off the
@@ -1874,6 +1952,7 @@ export function forkWorker(ds, promptInput, resumeOrTurnId = false) {
1874
1952
  // Use the decision recorded on the session (above), NOT the live bot flag, so
1875
1953
  // historical sessions never get retroactively sandboxed on restart.
1876
1954
  sandbox: ds.session.sandbox === true,
1955
+ sandboxPaths: ds.session.sandboxPaths ?? botCfg.sandboxPaths,
1877
1956
  sandboxHidePaths: ds.session.sandboxHidePaths ?? [],
1878
1957
  sandboxReadonlyPaths: ds.session.sandboxReadonlyPaths ?? [],
1879
1958
  sandboxNetwork: ds.session.sandboxNetwork !== false,
@@ -1893,10 +1972,15 @@ export function forkWorker(ds, promptInput, resumeOrTurnId = false) {
1893
1972
  // getSessionPersistentBackendType). A brand-new session (no stamp) resolves
1894
1973
  // from live config, so a dashboard backend switch only affects NEW sessions.
1895
1974
  backendType: resolvePairedSpawnBackendType(agentCfg.cliId, ds.session.backendType, botCfg.backendType, config.daemon.backendType),
1975
+ // Shared Herdr is not derivable from sessionId: preserve the exact host +
1976
+ // managed-agent affinity across daemon/worker replacement.
1977
+ persistentBackendTarget: ds.session.persistentBackendTarget,
1896
1978
  backendConfig: botCfg.riff,
1897
1979
  riffParentTaskId: ds.session.riffParentTaskId,
1898
1980
  riffRepoDirs: ds.session.riffRepoDirs,
1899
1981
  deferredScheduleRun: ds.session.deferredScheduleRun,
1982
+ ...(nativeSessionTitle ? { nativeSessionTitle } : {}),
1983
+ ...(nativeSessionTitlePrompt ? { nativeSessionTitlePrompt } : {}),
1900
1984
  prompt,
1901
1985
  ...(promptCodexAppInput ? { promptCodexAppInput } : {}),
1902
1986
  resume,
@@ -1937,7 +2021,7 @@ export function forkWorker(ds, promptInput, resumeOrTurnId = false) {
1937
2021
  sessionStore.updateSession(ds.session);
1938
2022
  }
1939
2023
  // Use shared handler for IPC messages and exit
1940
- setupWorkerHandlers(ds, worker, startupState);
2024
+ setupWorkerHandlers(ds, worker, startupState, workerGeneration);
1941
2025
  ds.worker = worker;
1942
2026
  ds.spawnedAt = Date.now();
1943
2027
  ds.cliVersion = currentCliVersion;
@@ -1967,11 +2051,57 @@ export function forkWorker(ds, promptInput, resumeOrTurnId = false) {
1967
2051
  recordOwnershipForDaemonSession(ds);
1968
2052
  }
1969
2053
  // ─── Shared worker IPC handler ──────────────────────────────────────────────
1970
- function setupWorkerHandlers(ds, worker, startupState = { ready: false, failureNotified: false }) {
2054
+ /**
2055
+ * Clear the stuck-warning authority markers WITHOUT patching the card. Used by
2056
+ * ACK paths (tui_keys_delivered / stuck_warning_expired) where the caller has
2057
+ * already resolved the card to its final state — we only need to drop the
2058
+ * nonce/cardId/turnId so a late duplicate click cannot re-inject keys.
2059
+ */
2060
+ export function clearStuckWarningAuthority(ds) {
2061
+ ds.stuckWarningCardId = undefined;
2062
+ ds.stuckWarningTurnId = undefined;
2063
+ ds.stuckWarningNonce = undefined;
2064
+ ds.stuckWarningPageType = undefined;
2065
+ ds.stuckWarningProcessing = false;
2066
+ ds.stuckWarningCliLifetime = undefined;
2067
+ }
2068
+ /**
2069
+ * Resolve any active stuck-warning card (PATCH it to "done") AND clear its
2070
+ * markers. Called from every path where the CLI can recover or be replaced:
2071
+ * prompt_ready, claude_exit, worker exit/kill/suspend/refork, and the worker's
2072
+ * own stale-card notification. Centralising here avoids missing an exit path
2073
+ * and leaving a clickable card that can inject keys into a different CLI.
2074
+ *
2075
+ * ACK paths (tui_keys_delivered / stuck_warning_expired) do NOT use this —
2076
+ * they patch the card themselves with a context-specific message and then call
2077
+ * clearStuckWarningAuthority() to drop the markers.
2078
+ */
2079
+ export function invalidateStuckWarning(ds, reason) {
2080
+ if (!ds.stuckWarningCardId && !ds.stuckWarningTurnId && ds.stuckWarningNonce === undefined)
2081
+ return;
2082
+ const t = tag(ds);
2083
+ if (ds.stuckWarningCardId) {
2084
+ const locDs = localeForBot(ds.larkAppId);
2085
+ const resolvedCard = buildTuiPromptResolvedCard(tr('card.action.tui_done', undefined, locDs), locDs);
2086
+ updateMessage(ds.larkAppId, ds.stuckWarningCardId, resolvedCard).catch(err => logger.debug(`[${t}] Failed to resolve stuck-warning card (${reason}): ${err}`));
2087
+ }
2088
+ logger.debug(`[${t}] invalidateStuckWarning (${reason}): turn=${ds.stuckWarningTurnId ?? 'none'} nonce=${ds.stuckWarningNonce ?? 'none'}`);
2089
+ clearStuckWarningAuthority(ds);
2090
+ }
2091
+ function setupWorkerHandlers(ds, worker, startupState = { ready: false, failureNotified: false }, reservedWorkerGeneration) {
1971
2092
  const cb = requireCallbacks();
1972
2093
  const t = tag(ds);
1973
- const workerGeneration = (ds.workerGeneration ?? 0) + 1;
1974
- ds.workerGeneration = workerGeneration;
2094
+ const workerGeneration = reservedWorkerGeneration
2095
+ ?? reserveWorkerGeneration(ds);
2096
+ if (ds.workerGeneration !== workerGeneration
2097
+ || ds.session.workerGeneration !== workerGeneration) {
2098
+ throw new Error('worker generation reservation changed before IPC setup');
2099
+ }
2100
+ // A new worker generation is starting. As a backstop, invalidate any
2101
+ // stuck-warning card posted by the previous generation — explicit kill/suspend/
2102
+ // exit paths should already have done this, but fork/refork/takeover paths
2103
+ // can leave a stale card that would otherwise inject keys into the new CLI.
2104
+ invalidateStuckWarning(ds, 'new_worker_generation');
1975
2105
  // Managed turn authority is issued by one concrete worker lifetime. A
1976
2106
  // replacement must advertise a fresh capability before daemon-mediated
1977
2107
  // exits may use it; carrying the old value across a restore/refork would
@@ -2063,6 +2193,29 @@ function setupWorkerHandlers(ds, worker, startupState = { ready: false, failureN
2063
2193
  worker.on('message', async (msg) => {
2064
2194
  const effectiveCliId = sessionCliId(ds, botCfg);
2065
2195
  switch (msg.type) {
2196
+ case 'persistent_backend_target': {
2197
+ ds.session.persistentBackendTarget = msg.target;
2198
+ sessionStore.updateSession(ds.session);
2199
+ break;
2200
+ }
2201
+ case 'turn_input_committed': {
2202
+ // Bind the receipt to the exact live worker generation. A late ACK
2203
+ // from a replaced worker cannot make the replacement appear to have
2204
+ // accepted this dispatch turn.
2205
+ if (ds.worker !== worker
2206
+ || ds.workerGeneration !== workerGeneration
2207
+ || ds.session.workerGeneration !== workerGeneration) {
2208
+ logger.warn(`[${t}] Ignored turn_input_committed from stale worker generation`);
2209
+ break;
2210
+ }
2211
+ if (recordDispatchInputCommit(ds.session, msg.turnId, workerGeneration)) {
2212
+ sessionStore.updateSession(ds.session);
2213
+ }
2214
+ else {
2215
+ logger.warn(`[${t}] Ignored unbound input commit turn=${msg.turnId.slice(0, 16)}`);
2216
+ }
2217
+ break;
2218
+ }
2066
2219
  case 'session_ready_ack': {
2067
2220
  if (ds.worker !== worker) {
2068
2221
  logger.warn(`[${t}] Ignored session_ready_ack from stale worker generation`);
@@ -2096,6 +2249,10 @@ function setupWorkerHandlers(ds, worker, startupState = { ready: false, failureN
2096
2249
  ds.workerViewToken = msg.viewToken ?? null;
2097
2250
  // Persist port so it can be reused after daemon restart
2098
2251
  ds.session.webPort = msg.port;
2252
+ // Dashboard「复现命令」:worker 上报本次冷启的近似复现命令。只存内存字段、
2253
+ // 绝不落盘(含凭证);warm reattach 时 worker 不重算,故为空——这是有意的
2254
+ // (reattach 不代表本次新算命令真的执行了)。worker 每次 ready 会重报。
2255
+ ds.spawnCommand = msg.spawnCommand;
2099
2256
  sessionStore.updateSession(ds.session);
2100
2257
  const readOnlyUrl = buildTerminalUrl(ds);
2101
2258
  const writeUrl = buildTerminalUrl(ds, { write: true });
@@ -2123,6 +2280,18 @@ function setupWorkerHandlers(ds, worker, startupState = { ready: false, failureN
2123
2280
  logger.info(`[${t}] Managed/silent turn — suppressing ready/streaming card output`);
2124
2281
  break;
2125
2282
  }
2283
+ // Pi can finish a very fast startup turn through `botmux send` before
2284
+ // Herdr reports idle and the worker emits ready. Posting the initial
2285
+ // card now would put a stale "Starting" card *after* the final reply,
2286
+ // with no later transcript output to withdraw it (the explicit send
2287
+ // intentionally suppresses bridge fallback). Keep the terminal/runtime
2288
+ // state above, but suppress this already-completed turn's card.
2289
+ if (msg.replyAlreadySent) {
2290
+ ds.streamCardPending = false;
2291
+ persistStreamCardState(ds);
2292
+ logger.info(`[${t}] Explicit reply landed before worker ready — skipping stale starting card`);
2293
+ break;
2294
+ }
2126
2295
  // Bot opted out of the streaming card: the terminal is up and the
2127
2296
  // final answer will still arrive via `botmux send`; just don't post the
2128
2297
  // live status card. (workerPort/token above are still set so the web
@@ -2277,6 +2446,8 @@ function setupWorkerHandlers(ds, worker, startupState = { ready: false, failureN
2277
2446
  break;
2278
2447
  }
2279
2448
  case 'prompt_ready': {
2449
+ if (ds.worker !== worker)
2450
+ break;
2280
2451
  logger.info(`[${t}] ${getCliDisplayName(effectiveCliId)} is ready for input`);
2281
2452
  // A live prompt means a (re)spawn reached a working CLI — clear the lazy
2282
2453
  // cold-resume marker set when we parked a crash diagnostic shell. The
@@ -2319,6 +2490,8 @@ function setupWorkerHandlers(ds, worker, startupState = { ready: false, failureN
2319
2490
  ...(followUpCodexAppInput ? { codexAppInput: followUpCodexAppInput } : {}),
2320
2491
  }, { codexAppInputAccepted: !!followUpCodexAppInput });
2321
2492
  }
2493
+ // CLI reached its prompt — any previously posted stuck warning is stale.
2494
+ invalidateStuckWarning(ds, 'prompt_ready');
2322
2495
  break;
2323
2496
  }
2324
2497
  case 'cli_session_id': {
@@ -2340,6 +2513,21 @@ function setupWorkerHandlers(ds, worker, startupState = { ready: false, failureN
2340
2513
  }
2341
2514
  break;
2342
2515
  }
2516
+ case 'native_session_title_generated': {
2517
+ if (ds.worker !== worker || ds.session.nativeSessionTitleUserDefined)
2518
+ break;
2519
+ const title = msg.title.trim();
2520
+ if (!title)
2521
+ break;
2522
+ ds.session.nativeSessionTitle = title;
2523
+ ds.session.nativeSessionTitleAwaitingContent = undefined;
2524
+ if (ds.initConfig) {
2525
+ ds.initConfig.nativeSessionTitle = title;
2526
+ ds.initConfig.nativeSessionTitlePrompt = undefined;
2527
+ }
2528
+ sessionStore.updateSession(ds.session);
2529
+ break;
2530
+ }
2343
2531
  case 'screen_update': {
2344
2532
  // Wait for `ready` (workerPort) before any card work — the read link
2345
2533
  // is the LOCAL log terminal for every backend including riff
@@ -2551,6 +2739,137 @@ function setupWorkerHandlers(ds, worker, startupState = { ready: false, failureN
2551
2739
  }
2552
2740
  break;
2553
2741
  }
2742
+ case 'stuck_warning': {
2743
+ // Ignore stuck_warning from a stale worker generation — the CLI may have
2744
+ // been replaced and a new worker owns the session now.
2745
+ if (ds.worker !== worker)
2746
+ break;
2747
+ // AI-free StuckDetector fired: a written input hasn't produced a
2748
+ // completed turn within the timeout AND the PTY has been quiet, AND the
2749
+ // snapshot matches a known Codex hook-review screen. The detector only
2750
+ // emits when it matches level 1 or level 2 — there is no "unknown
2751
+ // stall" path, so we always build an interactive card with the exact
2752
+ // keys the screen documents.
2753
+ // Dedup: skip if we already posted a warning for THIS turn, OR if a
2754
+ // stuck-warning card is already active (covers the no-turnId case where
2755
+ // a second stall fires before the first card is resolved).
2756
+ const isDuplicateTurn = msg.turnId !== undefined && ds.stuckWarningTurnId === msg.turnId;
2757
+ const hasActiveCard = !!ds.stuckWarningCardId;
2758
+ if (isDuplicateTurn || hasActiveCard) {
2759
+ logger.debug(`[${t}] Stuck warning dedup skipped (turn=${msg.turnId ?? 'none'}, activeCard=${hasActiveCard})`);
2760
+ break;
2761
+ }
2762
+ // Allocate a daemon-side nonce for this warning (NOT the worker's
2763
+ // generation — the daemon is the authority on which warning is active).
2764
+ // If the CLI recovers (prompt_ready) or the worker is replaced while the
2765
+ // POST is in flight, invalidateStuckWarning clears the nonce; when the
2766
+ // POST returns we check it is still current and, if not, resolve the card
2767
+ // immediately instead of registering it as active.
2768
+ // Allocate a daemon-side nonce for this warning from a monotonic
2769
+ // counter that is NEVER cleared (even when the active authority is
2770
+ // dropped). This prevents the nonce-reuse race: warning nonce=1 POST
2771
+ // awaits → prompt_ready clears active nonce → warning nonce=1 again
2772
+ // would let the old POST register against the new authority. With a
2773
+ // monotonic counter the second warning gets nonce=2, and the old
2774
+ // POST's nonce=1 check fails.
2775
+ const nonce = (ds.stuckWarningNonceCounter ?? 0) + 1;
2776
+ ds.stuckWarningNonceCounter = nonce;
2777
+ ds.stuckWarningNonce = nonce;
2778
+ ds.stuckWarningTurnId = msg.turnId;
2779
+ ds.stuckWarningCliLifetime = msg.cliLifetime;
2780
+ const pageType = msg.matchedPattern;
2781
+ ds.stuckWarningPageType = pageType;
2782
+ const secs = Math.round(msg.elapsedMs / 1000);
2783
+ logger.info(`[${t}] Stuck warning: turn unresolved for ${secs}s (${pageType}) nonce=${nonce}`);
2784
+ emitSessionLifecycleHook(ds, 'session.requires_attention', {
2785
+ reason: 'stuck_warning',
2786
+ elapsedMs: msg.elapsedMs,
2787
+ matchedPattern: pageType,
2788
+ });
2789
+ if (managedAuxUiSuppressed(msg.turnId, msg.dispatchAttempt))
2790
+ break;
2791
+ let description;
2792
+ let options;
2793
+ if (pageType === 'hook review level 2') {
2794
+ // Level 2 — per-hook review: t=trust, Esc=go back. No Enter here.
2795
+ description = `⚠️ Codex 卡在单项 hook 审核界面——消息已写入但 ${secs}s 未完成处理。\n\n选择操作以继续,或打开终端手动处理:`;
2796
+ options = [
2797
+ { label: 't', text: '信任 (trust)', selected: false, type: 'confirm', keys: ['t'] },
2798
+ { label: 'Esc', text: '返回 (go back)', selected: false, type: 'select', keys: ['Escape'] },
2799
+ ];
2800
+ }
2801
+ else {
2802
+ // Level 1 — hooks browser: t=trust all, Enter=review hooks, Esc=close.
2803
+ description = `⚠️ Codex 卡在 hook 审核界面——消息已写入但 ${secs}s 未完成处理。\n\n选择操作以继续,或打开终端手动处理:`;
2804
+ options = [
2805
+ { label: 't', text: '信任全部 (trust all)', selected: false, type: 'confirm', keys: ['t'] },
2806
+ { label: 'Enter', text: '逐项审核 (review hooks)', selected: false, type: 'select', keys: ['Enter'] },
2807
+ { label: 'Esc', text: '关闭 (close)', selected: false, type: 'select', keys: ['Escape'] },
2808
+ ];
2809
+ }
2810
+ try {
2811
+ const cardJson = buildTuiPromptCard(sessionAnchorId(ds), ds.session.sessionId, description, options, false, undefined, loc);
2812
+ const cardMsgId = await scopedReply(cardJson, 'interactive', msg.turnId);
2813
+ // Authority check: if the warning was invalidated (prompt_ready,
2814
+ // CLI exit, worker replace) OR the worker was replaced while the POST
2815
+ // was in flight, resolve the card immediately and do NOT register it
2816
+ // as active — a late click would otherwise inject keys into a
2817
+ // recovered/replaced CLI. Verify the full tuple: same worker process,
2818
+ // same worker generation, same nonce.
2819
+ if (ds.worker !== worker || ds.workerGeneration !== workerGeneration || ds.stuckWarningNonce !== nonce) {
2820
+ logger.debug(`[${t}] Stuck warning card POSTed but authority stale (nonce=${nonce}, current=${ds.stuckWarningNonce ?? 'none'}, workerGen=${workerGeneration}) — resolving`);
2821
+ const locDs = localeForBot(ds.larkAppId);
2822
+ const resolvedCard = buildTuiPromptResolvedCard(tr('card.action.tui_done', undefined, locDs), locDs);
2823
+ updateMessage(ds.larkAppId, cardMsgId, resolvedCard).catch(err => logger.debug(`[${t}] Failed to resolve stale stuck-warning card: ${err}`));
2824
+ break;
2825
+ }
2826
+ ds.stuckWarningCardId = cardMsgId;
2827
+ publishAttentionPatch(ds);
2828
+ }
2829
+ catch (err) {
2830
+ logger.warn(`[${t}] Failed to post stuck warning card: ${err}`);
2831
+ // Card send failed — clear markers ONLY if this nonce is still
2832
+ // current AND we still own the worker. A newer warning (nonce+1) may
2833
+ // have started while this POST was in flight; we must not wipe the
2834
+ // newer state.
2835
+ if (ds.worker === worker && ds.workerGeneration === workerGeneration && ds.stuckWarningNonce === nonce) {
2836
+ clearStuckWarningAuthority(ds);
2837
+ }
2838
+ }
2839
+ break;
2840
+ }
2841
+ case 'stuck_warning_expired': {
2842
+ // Ignore from a stale worker generation.
2843
+ if (ds.worker !== worker || ds.workerGeneration !== workerGeneration)
2844
+ break;
2845
+ // Worker refused to inject keys from a stuck-warning card because the
2846
+ // current screen no longer matches the page type the card was built for.
2847
+ // Resolve the card with a "page changed" message so the user knows the
2848
+ // action was NOT performed, then clear the authority.
2849
+ if (ds.stuckWarningNonce === msg.nonce && ds.stuckWarningCardId) {
2850
+ const locDs = localeForBot(ds.larkAppId);
2851
+ const resolvedCard = buildTuiPromptResolvedCard('页面已变化,未发送按键', locDs);
2852
+ updateMessage(ds.larkAppId, ds.stuckWarningCardId, resolvedCard).catch(err => logger.debug(`[${t}] Failed to update stuck-warning card on expired: ${err}`));
2853
+ clearStuckWarningAuthority(ds);
2854
+ }
2855
+ break;
2856
+ }
2857
+ case 'tui_keys_delivered': {
2858
+ // Ignore from a stale worker generation.
2859
+ if (ds.worker !== worker || ds.workerGeneration !== workerGeneration)
2860
+ break;
2861
+ // Worker confirmed the keys were written to the PTY. Clear the card
2862
+ // authority and render success. We only do this AFTER the worker ACK
2863
+ // (not on click), so a rejected click (stuck_warning_expired) does not
2864
+ // falsely report success.
2865
+ if (ds.stuckWarningNonce === msg.nonce && ds.stuckWarningCardId) {
2866
+ const locDs = localeForBot(ds.larkAppId);
2867
+ const resolvedCard = buildTuiPromptResolvedCard(tr('card.action.tui_done', undefined, locDs), locDs);
2868
+ updateMessage(ds.larkAppId, ds.stuckWarningCardId, resolvedCard).catch(err => logger.debug(`[${t}] Failed to resolve stuck-warning card on delivered: ${err}`));
2869
+ clearStuckWarningAuthority(ds);
2870
+ }
2871
+ break;
2872
+ }
2554
2873
  case 'claude_exit': {
2555
2874
  // CLI-generation authority must not outlive the concrete worker/CLI
2556
2875
  // pair that issued it. A delayed message from a replaced Node worker
@@ -2560,6 +2879,10 @@ function setupWorkerHandlers(ds, worker, startupState = { ready: false, failureN
2560
2879
  break;
2561
2880
  }
2562
2881
  ds.managedTurnOrigin = undefined;
2882
+ // The worker/CLI generation ended. Disable an outstanding stuck-warning
2883
+ // card before any replacement worker can be attached; otherwise a late
2884
+ // click could inject its keys into the replacement CLI.
2885
+ invalidateStuckWarning(ds, 'claude_exit');
2563
2886
  logger.info(`[${t}] ${getCliDisplayName(effectiveCliId)} exited (code: ${msg.code}, signal: ${msg.signal})`);
2564
2887
  ds.hasHistory = true;
2565
2888
  try {
@@ -2979,6 +3302,19 @@ function setupWorkerHandlers(ds, worker, startupState = { ready: false, failureN
2979
3302
  ds.worker = null;
2980
3303
  ds.workerPort = null;
2981
3304
  ds.managedTurnOrigin = undefined;
3305
+ // This worker generation is gone. Invalidate any stuck-warning card it
3306
+ // posted so a late click cannot inject keys into a replacement worker.
3307
+ invalidateStuckWarning(ds, 'worker_exit');
3308
+ // Fence this lifetime before a polling dispatcher can observe its last
3309
+ // ACK. Keeping the old receipt is useful audit evidence, but the
3310
+ // persisted current generation advances immediately so it cannot count
3311
+ // as acceptance after the worker has died. A stale takeover worker never
3312
+ // enters this branch and therefore cannot fence the replacement.
3313
+ const fencedGeneration = Math.max(workerGeneration, ds.workerGeneration ?? 0, ds.session.workerGeneration ?? 0) + 1;
3314
+ ds.workerGeneration = fencedGeneration;
3315
+ ds.session.workerGeneration = fencedGeneration;
3316
+ ds.session.pid = undefined;
3317
+ sessionStore.updateSession(ds.session);
2982
3318
  }
2983
3319
  try {
2984
3320
  const notified = cb.onWorkerExit?.(ds, {
@@ -3345,6 +3681,52 @@ export const __testOnly_setupWorkerHandlers = setupWorkerHandlers;
3345
3681
  export const __testOnly_finishTurnReactions = finishTurnReactions;
3346
3682
  export const __testOnly_finalOutputDedupeKey = finalOutputDedupeKey;
3347
3683
  // ─── Fork adopt worker ──────────────────────────────────────────────────────
3684
+ function reserveWorkerGeneration(ds) {
3685
+ const previousDaemonGeneration = ds.workerGeneration;
3686
+ const previousSessionGeneration = ds.session.workerGeneration;
3687
+ const workerGeneration = Math.max(previousDaemonGeneration ?? 0, previousSessionGeneration ?? 0) + 1;
3688
+ ds.workerGeneration = workerGeneration;
3689
+ ds.session.workerGeneration = workerGeneration;
3690
+ try {
3691
+ sessionStore.updateSession(ds.session);
3692
+ }
3693
+ catch (error) {
3694
+ if (previousDaemonGeneration === undefined)
3695
+ delete ds.workerGeneration;
3696
+ else
3697
+ ds.workerGeneration = previousDaemonGeneration;
3698
+ if (previousSessionGeneration === undefined)
3699
+ delete ds.session.workerGeneration;
3700
+ else
3701
+ ds.session.workerGeneration = previousSessionGeneration;
3702
+ throw error;
3703
+ }
3704
+ return workerGeneration;
3705
+ }
3706
+ /**
3707
+ * Is the file sandbox active such that /adopt must be refused? A sandbox can
3708
+ * only be established at spawn time (bwrap wrap / Seatbelt profile); adopt
3709
+ * attaches to an ALREADY-running host process, so it could only ever run
3710
+ * UNsandboxed. From-strict UNION of every source that can require isolation:
3711
+ * - live per-bot `sandbox` / legacy `readIsolation`,
3712
+ * - the global `BOTMUX_SANDBOX=1` (sandboxEnabled()),
3713
+ * - the session's FROZEN sandbox decision (`session.sandbox`, recorded at
3714
+ * creation). forkWorker treats the frozen decision as authoritative, so a
3715
+ * session created sandboxed must stay un-adoptable even if the admin later
3716
+ * toggles the bot's global flag OFF — otherwise its `/adopt` would attach to
3717
+ * an unsandboxed live process. The reverse (bot=true / session=false) is
3718
+ * also caught by the union, which is the safe direction.
3719
+ * Callers reject at the ENTRY point (before persisting `adoptedFrom` / replying
3720
+ * "adopted") — see command-handler's `/adopt`, the adopt_select card, and the
3721
+ * session-manager restore branch. Resume/cold-start is unaffected: those spawn
3722
+ * a fresh CLI, which the sandbox wraps normally.
3723
+ */
3724
+ export function adoptSandboxBlocked(botCfg, session) {
3725
+ return botCfg.sandbox === true
3726
+ || botCfg.readIsolation === true
3727
+ || session?.sandbox === true
3728
+ || sandboxEnabled();
3729
+ }
3348
3730
  export function forkAdoptWorker(ds, opts) {
3349
3731
  const cb = requireCallbacks();
3350
3732
  const workerPath = join(__dirname, '..', 'worker.js');
@@ -3354,14 +3736,35 @@ export function forkAdoptWorker(ds, opts) {
3354
3736
  throw new Error('forkAdoptWorker called without adoptedFrom');
3355
3737
  const bot = getBot(ds.larkAppId);
3356
3738
  const botCfg = bot.config;
3357
- // Read isolation cannot be applied to an already-running CLI (adopt attaches
3358
- // to an existing pane; we can't inject --settings into it). Refuse to adopt an
3359
- // isolated bot rather than run it unisolated it will cold-start (isolated)
3360
- // via forkWorker on the next message instead. (Codex review #2, fail-closed.)
3361
- if (botCfg.readIsolation === true) {
3362
- logger.warn(`[${t}] read-isolation bot: refusing to adopt existing CLI (would run unisolated); will cold-start isolated on next message`);
3739
+ // A file sandbox cannot be applied to an already-running CLI: adopt ATTACHES
3740
+ // to an existing host pane/process, and confinement (bwrap wrap on Linux /
3741
+ // Seatbelt profile on macOS) can only be established at spawn time — there is
3742
+ // no way to retro-fit a sandbox around a live process. Refuse to adopt when
3743
+ // the sandbox is active (live bot flag OR the session's FROZEN decision)
3744
+ // rather than run it UNsandboxed. Real adopt entry points (`/adopt`, the
3745
+ // adopt_select card) reject BEFORE persisting `adoptedFrom`, and the
3746
+ // session-manager restore branch converts a sandbox adopt to a cold-start
3747
+ // before it ever reaches here; this is the last-line fail-closed backstop,
3748
+ // which also strips any stale adopt metadata it is handed.
3749
+ if (adoptSandboxBlocked(botCfg, ds.session)) {
3750
+ logger.warn(`[${t}] sandbox-enabled session: refusing to adopt existing CLI (would run unsandboxed)`);
3751
+ // Strip stale adopt state so the session doesn't linger as a worker=null
3752
+ // pseudo-adopt whose NEXT message
3753
+ // would still be routed as a bridge/adopt session. It cold-starts sandboxed
3754
+ // via the normal resume path instead.
3755
+ ds.adoptedFrom = undefined;
3756
+ if (ds.session.adoptedFrom) {
3757
+ ds.session.adoptedFrom = undefined;
3758
+ try {
3759
+ sessionStore.updateSession(ds.session);
3760
+ }
3761
+ catch { /* best-effort */ }
3762
+ }
3363
3763
  return;
3364
3764
  }
3765
+ // Reserve before replacing an existing bridge worker for the same reason as
3766
+ // forkWorker: persistence failure must leave the old lifetime untouched.
3767
+ const workerGeneration = reserveWorkerGeneration(ds);
3365
3768
  // Guard against double-fork
3366
3769
  if (ds.worker && !ds.worker.killed) {
3367
3770
  logger.warn(`[${t}] Worker already running, killing before adopt-fork`);
@@ -3541,7 +3944,7 @@ export function forkAdoptWorker(ds, opts) {
3541
3944
  sessionStore.updateSession(ds.session);
3542
3945
  }
3543
3946
  // Use shared handler
3544
- setupWorkerHandlers(ds, worker, startupState);
3947
+ setupWorkerHandlers(ds, worker, startupState, workerGeneration);
3545
3948
  ds.worker = worker;
3546
3949
  ds.spawnedAt = Date.now();
3547
3950
  ds.cliVersion = '';
@@ -3705,9 +4108,17 @@ function cleanupPersistentBackendSessions(backendType, activeSessions_) {
3705
4108
  const currentCliId = config.daemon.cliId;
3706
4109
  if (!multiBot && lastCliId && lastCliId !== currentCliId) {
3707
4110
  logger.info(`CLI_ID changed (${lastCliId} → ${currentCliId}), killing all ${backendType} sessions`);
4111
+ // Legacy per-topic hosts are still enumerable by bmx-* name.
3708
4112
  for (const name of backend.listBotmuxSessions()) {
3709
4113
  backend.killSession(name);
3710
4114
  }
4115
+ // Machine-wide Herdr agents are not separate bmx-* sessions. Tear down
4116
+ // each persisted managed target precisely so a CLI switch cannot reattach
4117
+ // the old executable, while never stopping the shared `botmux` host or an
4118
+ // explicitly adopted user pane.
4119
+ for (const target of managedTargetsForCliChange(backendType, activeSessions_)) {
4120
+ killPersistentBackendTarget(target);
4121
+ }
3711
4122
  }
3712
4123
  else {
3713
4124
  const activeNames = new Set(activeSessions_.map(s => backend.sessionName(s.sessionId)));
@@ -3730,9 +4141,12 @@ function cleanupPersistentBackendSessions(backendType, activeSessions_) {
3730
4141
  continue;
3731
4142
  }
3732
4143
  if (botCliId && sessionCliId !== botCliId) {
3733
- const name = backend.sessionName(session.sessionId);
3734
- logger.info(`CLI mismatch for ${session.sessionId.substring(0, 8)} (session=${sessionCliId}, bot=${botCliId}), killing ${backendType} ${name}`);
3735
- backend.killSession(name);
4144
+ const target = resolvePersistentBackendTarget(backendType, session.sessionId, session.persistentBackendTarget);
4145
+ const label = target.backendType === 'herdr' && target.agentName
4146
+ ? `${target.sessionName}/${target.agentName}`
4147
+ : target.sessionName;
4148
+ logger.info(`CLI mismatch for ${session.sessionId.substring(0, 8)} (session=${sessionCliId}, bot=${botCliId}), killing ${backendType} ${label}`);
4149
+ killPersistentBackendTarget(target);
3736
4150
  }
3737
4151
  }
3738
4152
  }