botmux 2.111.0 → 2.112.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 (1041) hide show
  1. package/README.en.md +19 -7
  2. package/README.md +19 -7
  3. package/dist/adapters/adopt-route.d.ts.map +1 -1
  4. package/dist/adapters/adopt-route.js +2 -1
  5. package/dist/adapters/adopt-route.js.map +1 -1
  6. package/dist/adapters/backend/herdr-backend.d.ts +29 -0
  7. package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
  8. package/dist/adapters/backend/herdr-backend.js +167 -0
  9. package/dist/adapters/backend/herdr-backend.js.map +1 -1
  10. package/dist/adapters/backend/riff-backend.js +2 -2
  11. package/dist/adapters/backend/sandbox.d.ts +61 -6
  12. package/dist/adapters/backend/sandbox.d.ts.map +1 -1
  13. package/dist/adapters/backend/sandbox.js +235 -43
  14. package/dist/adapters/backend/sandbox.js.map +1 -1
  15. package/dist/adapters/backend/tmux-backend.d.ts +1 -0
  16. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  17. package/dist/adapters/backend/tmux-backend.js +1 -0
  18. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  19. package/dist/adapters/cli/claude-code.d.ts +2 -0
  20. package/dist/adapters/cli/claude-code.d.ts.map +1 -1
  21. package/dist/adapters/cli/claude-code.js +7 -0
  22. package/dist/adapters/cli/claude-code.js.map +1 -1
  23. package/dist/adapters/cli/codex-app-turn.d.ts +16 -0
  24. package/dist/adapters/cli/codex-app-turn.d.ts.map +1 -1
  25. package/dist/adapters/cli/codex-app-turn.js +26 -0
  26. package/dist/adapters/cli/codex-app-turn.js.map +1 -1
  27. package/dist/adapters/cli/codex.d.ts.map +1 -1
  28. package/dist/adapters/cli/codex.js +1 -0
  29. package/dist/adapters/cli/codex.js.map +1 -1
  30. package/dist/adapters/cli/grok.d.ts +8 -6
  31. package/dist/adapters/cli/grok.d.ts.map +1 -1
  32. package/dist/adapters/cli/grok.js +14 -6
  33. package/dist/adapters/cli/grok.js.map +1 -1
  34. package/dist/adapters/cli/read-isolation.d.ts +40 -14
  35. package/dist/adapters/cli/read-isolation.d.ts.map +1 -1
  36. package/dist/adapters/cli/read-isolation.js +116 -19
  37. package/dist/adapters/cli/read-isolation.js.map +1 -1
  38. package/dist/adapters/cli/runner-control-channel.d.ts +32 -0
  39. package/dist/adapters/cli/runner-control-channel.d.ts.map +1 -0
  40. package/dist/adapters/cli/runner-control-channel.js +104 -0
  41. package/dist/adapters/cli/runner-control-channel.js.map +1 -0
  42. package/dist/adapters/cli/shared-hints.d.ts +1 -1
  43. package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
  44. package/dist/adapters/cli/shared-hints.js +9 -0
  45. package/dist/adapters/cli/shared-hints.js.map +1 -1
  46. package/dist/adapters/cli/traex.d.ts.map +1 -1
  47. package/dist/adapters/cli/traex.js +100 -64
  48. package/dist/adapters/cli/traex.js.map +1 -1
  49. package/dist/adapters/cli/types.d.ts +13 -0
  50. package/dist/adapters/cli/types.d.ts.map +1 -1
  51. package/dist/bot-registry.d.ts +36 -27
  52. package/dist/bot-registry.d.ts.map +1 -1
  53. package/dist/bot-registry.js +354 -8
  54. package/dist/bot-registry.js.map +1 -1
  55. package/dist/cli/saved-workflow.d.ts +25 -0
  56. package/dist/cli/saved-workflow.d.ts.map +1 -0
  57. package/dist/cli/saved-workflow.js +338 -0
  58. package/dist/cli/saved-workflow.js.map +1 -0
  59. package/dist/cli/send-dispatch.d.ts +18 -3
  60. package/dist/cli/send-dispatch.d.ts.map +1 -1
  61. package/dist/cli/send-dispatch.js +22 -3
  62. package/dist/cli/send-dispatch.js.map +1 -1
  63. package/dist/cli/v3-run-cancel.d.ts +47 -0
  64. package/dist/cli/v3-run-cancel.d.ts.map +1 -0
  65. package/dist/cli/v3-run-cancel.js +96 -0
  66. package/dist/cli/v3-run-cancel.js.map +1 -0
  67. package/dist/cli/vc-agent.d.ts.map +1 -1
  68. package/dist/cli/vc-agent.js +54 -1
  69. package/dist/cli/vc-agent.js.map +1 -1
  70. package/dist/cli/workflow-migration.d.ts +39 -0
  71. package/dist/cli/workflow-migration.d.ts.map +1 -0
  72. package/dist/cli/workflow-migration.js +302 -0
  73. package/dist/cli/workflow-migration.js.map +1 -0
  74. package/dist/cli/workflow-run-archive.d.ts +29 -0
  75. package/dist/cli/workflow-run-archive.d.ts.map +1 -0
  76. package/dist/cli/workflow-run-archive.js +183 -0
  77. package/dist/cli/workflow-run-archive.js.map +1 -0
  78. package/dist/cli/workflow.d.ts +5 -10
  79. package/dist/cli/workflow.d.ts.map +1 -1
  80. package/dist/cli/workflow.js +85 -764
  81. package/dist/cli/workflow.js.map +1 -1
  82. package/dist/cli.d.ts.map +1 -1
  83. package/dist/cli.js +1018 -191
  84. package/dist/cli.js.map +1 -1
  85. package/dist/codex-app-runner.js +30 -21
  86. package/dist/codex-app-runner.js.map +1 -1
  87. package/dist/core/command-handler.d.ts.map +1 -1
  88. package/dist/core/command-handler.js +21 -9
  89. package/dist/core/command-handler.js.map +1 -1
  90. package/dist/core/current-turn-provenance.d.ts +39 -0
  91. package/dist/core/current-turn-provenance.d.ts.map +1 -0
  92. package/dist/core/current-turn-provenance.js +122 -0
  93. package/dist/core/current-turn-provenance.js.map +1 -0
  94. package/dist/core/daemon-ipc-auth.d.ts +20 -0
  95. package/dist/core/daemon-ipc-auth.d.ts.map +1 -0
  96. package/dist/core/daemon-ipc-auth.js +44 -0
  97. package/dist/core/daemon-ipc-auth.js.map +1 -0
  98. package/dist/core/daemon-ipc-session-auth.d.ts +37 -0
  99. package/dist/core/daemon-ipc-session-auth.d.ts.map +1 -0
  100. package/dist/core/daemon-ipc-session-auth.js +35 -0
  101. package/dist/core/daemon-ipc-session-auth.js.map +1 -0
  102. package/dist/core/dashboard-command/index.d.ts +0 -2
  103. package/dist/core/dashboard-command/index.d.ts.map +1 -1
  104. package/dist/core/dashboard-command/index.js +2 -2
  105. package/dist/core/dashboard-command/index.js.map +1 -1
  106. package/dist/core/dashboard-command/stub.d.ts +1 -1
  107. package/dist/core/dashboard-command/stub.d.ts.map +1 -1
  108. package/dist/core/dashboard-command/stub.js +0 -1
  109. package/dist/core/dashboard-command/stub.js.map +1 -1
  110. package/dist/core/dashboard-ipc-server.d.ts +11 -2
  111. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  112. package/dist/core/dashboard-ipc-server.js +266 -41
  113. package/dist/core/dashboard-ipc-server.js.map +1 -1
  114. package/dist/core/dashboard-secret.d.ts +3 -0
  115. package/dist/core/dashboard-secret.d.ts.map +1 -0
  116. package/dist/core/dashboard-secret.js +7 -0
  117. package/dist/core/dashboard-secret.js.map +1 -0
  118. package/dist/core/data-dir.d.ts +24 -0
  119. package/dist/core/data-dir.d.ts.map +1 -0
  120. package/dist/core/data-dir.js +48 -0
  121. package/dist/core/data-dir.js.map +1 -0
  122. package/dist/core/inflight-input-tracker.d.ts +4 -2
  123. package/dist/core/inflight-input-tracker.d.ts.map +1 -1
  124. package/dist/core/inflight-input-tracker.js +6 -5
  125. package/dist/core/inflight-input-tracker.js.map +1 -1
  126. package/dist/core/inject-queue-policy.d.ts +70 -0
  127. package/dist/core/inject-queue-policy.d.ts.map +1 -0
  128. package/dist/core/inject-queue-policy.js +30 -0
  129. package/dist/core/inject-queue-policy.js.map +1 -0
  130. package/dist/core/maintenance.d.ts +1 -1
  131. package/dist/core/maintenance.d.ts.map +1 -1
  132. package/dist/core/maintenance.js +3 -2
  133. package/dist/core/maintenance.js.map +1 -1
  134. package/dist/core/managed-origin-capability.d.ts +30 -0
  135. package/dist/core/managed-origin-capability.d.ts.map +1 -0
  136. package/dist/core/managed-origin-capability.js +88 -0
  137. package/dist/core/managed-origin-capability.js.map +1 -0
  138. package/dist/core/role-library.d.ts +15 -0
  139. package/dist/core/role-library.d.ts.map +1 -0
  140. package/dist/core/role-library.js +75 -0
  141. package/dist/core/role-library.js.map +1 -0
  142. package/dist/core/session-cwd.d.ts +14 -0
  143. package/dist/core/session-cwd.d.ts.map +1 -0
  144. package/dist/core/session-cwd.js +17 -0
  145. package/dist/core/session-cwd.js.map +1 -0
  146. package/dist/core/session-manager.d.ts +4 -1
  147. package/dist/core/session-manager.d.ts.map +1 -1
  148. package/dist/core/session-manager.js +27 -8
  149. package/dist/core/session-manager.js.map +1 -1
  150. package/dist/core/session-marker.d.ts +22 -0
  151. package/dist/core/session-marker.d.ts.map +1 -1
  152. package/dist/core/session-marker.js +185 -20
  153. package/dist/core/session-marker.js.map +1 -1
  154. package/dist/core/slash-inject.d.ts +15 -0
  155. package/dist/core/slash-inject.d.ts.map +1 -0
  156. package/dist/core/slash-inject.js +25 -0
  157. package/dist/core/slash-inject.js.map +1 -0
  158. package/dist/core/terminal-url.d.ts +1 -0
  159. package/dist/core/terminal-url.d.ts.map +1 -1
  160. package/dist/core/terminal-url.js +14 -8
  161. package/dist/core/terminal-url.js.map +1 -1
  162. package/dist/core/terminal-write-auth.d.ts +26 -0
  163. package/dist/core/terminal-write-auth.d.ts.map +1 -1
  164. package/dist/core/terminal-write-auth.js +48 -3
  165. package/dist/core/terminal-write-auth.js.map +1 -1
  166. package/dist/core/trigger-session.d.ts +24 -1
  167. package/dist/core/trigger-session.d.ts.map +1 -1
  168. package/dist/core/trigger-session.js +106 -15
  169. package/dist/core/trigger-session.js.map +1 -1
  170. package/dist/core/types.d.ts +26 -1
  171. package/dist/core/types.d.ts.map +1 -1
  172. package/dist/core/types.js +10 -0
  173. package/dist/core/types.js.map +1 -1
  174. package/dist/core/worker-pool.d.ts +65 -2
  175. package/dist/core/worker-pool.d.ts.map +1 -1
  176. package/dist/core/worker-pool.js +459 -39
  177. package/dist/core/worker-pool.js.map +1 -1
  178. package/dist/daemon.d.ts +249 -8
  179. package/dist/daemon.d.ts.map +1 -1
  180. package/dist/daemon.js +7383 -1195
  181. package/dist/daemon.js.map +1 -1
  182. package/dist/dashboard/aggregator.d.ts +1 -1
  183. package/dist/dashboard/aggregator.d.ts.map +1 -1
  184. package/dist/dashboard/aggregator.js +2 -2
  185. package/dist/dashboard/aggregator.js.map +1 -1
  186. package/dist/dashboard/auth.d.ts +3 -8
  187. package/dist/dashboard/auth.d.ts.map +1 -1
  188. package/dist/dashboard/auth.js +63 -22
  189. package/dist/dashboard/auth.js.map +1 -1
  190. package/dist/dashboard/bot-onboarding.d.ts +5 -0
  191. package/dist/dashboard/bot-onboarding.d.ts.map +1 -1
  192. package/dist/dashboard/bot-onboarding.js +37 -0
  193. package/dist/dashboard/bot-onboarding.js.map +1 -1
  194. package/dist/dashboard/connector-api.d.ts.map +1 -1
  195. package/dist/dashboard/connector-api.js +42 -2
  196. package/dist/dashboard/connector-api.js.map +1 -1
  197. package/dist/dashboard/daemon-internal-api.d.ts +0 -2
  198. package/dist/dashboard/daemon-internal-api.d.ts.map +1 -1
  199. package/dist/dashboard/daemon-internal-api.js +26 -93
  200. package/dist/dashboard/daemon-internal-api.js.map +1 -1
  201. package/dist/dashboard/daemon-internal-client.d.ts.map +1 -1
  202. package/dist/dashboard/daemon-internal-client.js +2 -3
  203. package/dist/dashboard/daemon-internal-client.js.map +1 -1
  204. package/dist/dashboard/federation-api.js +1 -1
  205. package/dist/dashboard/federation-api.js.map +1 -1
  206. package/dist/dashboard/federation-spoke-api.js +1 -1
  207. package/dist/dashboard/federation-spoke-api.js.map +1 -1
  208. package/dist/dashboard/history-senders.d.ts +3 -0
  209. package/dist/dashboard/history-senders.d.ts.map +1 -1
  210. package/dist/dashboard/history-senders.js.map +1 -1
  211. package/dist/dashboard/http.d.ts +4 -0
  212. package/dist/dashboard/http.d.ts.map +1 -0
  213. package/dist/dashboard/http.js +6 -0
  214. package/dist/dashboard/http.js.map +1 -0
  215. package/dist/dashboard/registry.d.ts +4 -0
  216. package/dist/dashboard/registry.d.ts.map +1 -1
  217. package/dist/dashboard/registry.js.map +1 -1
  218. package/dist/dashboard/trigger-api.js +1 -1
  219. package/dist/dashboard/trigger-api.js.map +1 -1
  220. package/dist/dashboard/v3-runs-api.d.ts +13 -10
  221. package/dist/dashboard/v3-runs-api.d.ts.map +1 -1
  222. package/dist/dashboard/v3-runs-api.js +121 -4
  223. package/dist/dashboard/v3-runs-api.js.map +1 -1
  224. package/dist/dashboard/vc-consumer-profiles-api.d.ts +105 -0
  225. package/dist/dashboard/vc-consumer-profiles-api.d.ts.map +1 -0
  226. package/dist/dashboard/vc-consumer-profiles-api.js +312 -0
  227. package/dist/dashboard/vc-consumer-profiles-api.js.map +1 -0
  228. package/dist/dashboard/web/app.d.ts.map +1 -1
  229. package/dist/dashboard/web/app.js +7 -6
  230. package/dist/dashboard/web/app.js.map +1 -1
  231. package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
  232. package/dist/dashboard/web/bot-defaults-page.js +9 -14
  233. package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
  234. package/dist/dashboard/web/bot-onboarding.d.ts.map +1 -1
  235. package/dist/dashboard/web/bot-onboarding.js +7 -3
  236. package/dist/dashboard/web/bot-onboarding.js.map +1 -1
  237. package/dist/dashboard/web/connectors-page.d.ts +5 -0
  238. package/dist/dashboard/web/connectors-page.d.ts.map +1 -1
  239. package/dist/dashboard/web/connectors-page.js +16 -12
  240. package/dist/dashboard/web/connectors-page.js.map +1 -1
  241. package/dist/dashboard/web/create-session-entry.d.ts +4 -0
  242. package/dist/dashboard/web/create-session-entry.d.ts.map +1 -0
  243. package/dist/dashboard/web/create-session-entry.js +17 -0
  244. package/dist/dashboard/web/create-session-entry.js.map +1 -0
  245. package/dist/dashboard/web/dashboard-components.d.ts +5 -0
  246. package/dist/dashboard/web/dashboard-components.d.ts.map +1 -1
  247. package/dist/dashboard/web/dashboard-components.js +29 -4
  248. package/dist/dashboard/web/dashboard-components.js.map +1 -1
  249. package/dist/dashboard/web/dashboard-routes.d.ts.map +1 -1
  250. package/dist/dashboard/web/dashboard-routes.js +0 -1
  251. package/dist/dashboard/web/dashboard-routes.js.map +1 -1
  252. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  253. package/dist/dashboard/web/i18n.js +192 -382
  254. package/dist/dashboard/web/i18n.js.map +1 -1
  255. package/dist/dashboard/web/preferences.d.ts +3 -0
  256. package/dist/dashboard/web/preferences.d.ts.map +1 -1
  257. package/dist/dashboard/web/preferences.js +18 -0
  258. package/dist/dashboard/web/preferences.js.map +1 -1
  259. package/dist/dashboard/web/schedules-page.d.ts.map +1 -1
  260. package/dist/dashboard/web/schedules-page.js +4 -1
  261. package/dist/dashboard/web/schedules-page.js.map +1 -1
  262. package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
  263. package/dist/dashboard/web/sessions-page.js +64 -18
  264. package/dist/dashboard/web/sessions-page.js.map +1 -1
  265. package/dist/dashboard/web/settings-page.d.ts.map +1 -1
  266. package/dist/dashboard/web/settings-page.js +2 -1
  267. package/dist/dashboard/web/settings-page.js.map +1 -1
  268. package/dist/dashboard/web/v3-api.d.ts +12 -2
  269. package/dist/dashboard/web/v3-api.d.ts.map +1 -1
  270. package/dist/dashboard/web/v3-api.js +34 -6
  271. package/dist/dashboard/web/v3-api.js.map +1 -1
  272. package/dist/dashboard/web/v3-components.d.ts +6 -0
  273. package/dist/dashboard/web/v3-components.d.ts.map +1 -1
  274. package/dist/dashboard/web/v3-components.js +66 -23
  275. package/dist/dashboard/web/v3-components.js.map +1 -1
  276. package/dist/dashboard/web/v3-model.js +1 -1
  277. package/dist/dashboard/web/v3-model.js.map +1 -1
  278. package/dist/dashboard/web/v3-terminal.js +2 -2
  279. package/dist/dashboard/web/v3-terminal.js.map +1 -1
  280. package/dist/dashboard/web/vc-consumer-profiles-section.d.ts +19 -0
  281. package/dist/dashboard/web/vc-consumer-profiles-section.d.ts.map +1 -0
  282. package/dist/dashboard/web/vc-consumer-profiles-section.js +382 -0
  283. package/dist/dashboard/web/vc-consumer-profiles-section.js.map +1 -0
  284. package/dist/dashboard/webhook-routes.d.ts.map +1 -1
  285. package/dist/dashboard/webhook-routes.js +29 -6
  286. package/dist/dashboard/webhook-routes.js.map +1 -1
  287. package/dist/dashboard-web/app.js +1 -1
  288. package/dist/dashboard-web/chunks/bot-defaults-page-RYBL4ZYE.js +4 -0
  289. package/dist/dashboard-web/chunks/{chunk-OEJIT66L.js → chunk-CMDPZOPK.js} +1 -1
  290. package/dist/dashboard-web/chunks/chunk-I35DXGM4.js +1 -0
  291. package/dist/dashboard-web/chunks/chunk-NYZHARIU.js +47 -0
  292. package/dist/dashboard-web/chunks/chunk-SZOB6VK2.js +1 -0
  293. package/dist/dashboard-web/chunks/{chunk-YQFXGRSD.js → chunk-YBOHL7T7.js} +1 -1
  294. package/dist/dashboard-web/chunks/chunk-YWCPDL4S.js +1 -0
  295. package/dist/dashboard-web/chunks/connectors-page-DAMP7TTV.js +1 -0
  296. package/dist/dashboard-web/chunks/{groups-page-5AJPPJH2.js → groups-page-BPFA356N.js} +1 -1
  297. package/dist/dashboard-web/chunks/{insights-page-R3DFECX4.js → insights-page-SHOPHTWY.js} +1 -1
  298. package/dist/dashboard-web/chunks/{monitor-room-NLMKJLDY.js → monitor-room-PXPJGZCN.js} +1 -1
  299. package/dist/dashboard-web/chunks/{monitoring-page-EHSOX4GV.js → monitoring-page-4DF54M5Y.js} +1 -1
  300. package/dist/dashboard-web/chunks/{office-page-OXJKFJ4R.js → office-page-6OHU6KMZ.js} +1 -1
  301. package/dist/dashboard-web/chunks/{overview-page-GJHIVXJF.js → overview-page-IHIZ6F5M.js} +1 -1
  302. package/dist/dashboard-web/chunks/{roles-page-6ZODQISW.js → roles-page-JYTBYKEF.js} +1 -1
  303. package/dist/dashboard-web/chunks/{schedules-page-DAGMHVCH.js → schedules-page-WJ72T7KV.js} +1 -1
  304. package/dist/dashboard-web/chunks/sessions-page-QC56OIAI.js +3 -0
  305. package/dist/dashboard-web/chunks/settings-page-TCKWEXTN.js +2 -0
  306. package/dist/dashboard-web/chunks/{skills-page-J7EVUHXP.js → skills-page-RJJNIECW.js} +1 -1
  307. package/dist/dashboard-web/chunks/{team-federation-page-YM5JBIII.js → team-federation-page-ZMDX3E7K.js} +1 -1
  308. package/dist/dashboard-web/chunks/v3-page-VLLAUQ33.js +94 -0
  309. package/dist/dashboard-web/chunks/{whiteboards-page-75N2U6W4.js → whiteboards-page-NAO53CKM.js} +1 -1
  310. package/dist/dashboard-web/style.css +363 -274
  311. package/dist/dashboard.js +194 -78
  312. package/dist/dashboard.js.map +1 -1
  313. package/dist/global-config.d.ts +1 -1
  314. package/dist/i18n/en.d.ts.map +1 -1
  315. package/dist/i18n/en.js +12 -87
  316. package/dist/i18n/en.js.map +1 -1
  317. package/dist/i18n/zh.d.ts.map +1 -1
  318. package/dist/i18n/zh.js +12 -87
  319. package/dist/i18n/zh.js.map +1 -1
  320. package/dist/im/lark/brand-template.d.ts +15 -0
  321. package/dist/im/lark/brand-template.d.ts.map +1 -0
  322. package/dist/im/lark/brand-template.js +114 -0
  323. package/dist/im/lark/brand-template.js.map +1 -0
  324. package/dist/im/lark/card-handler.d.ts +6 -3
  325. package/dist/im/lark/card-handler.d.ts.map +1 -1
  326. package/dist/im/lark/card-handler.js +23 -39
  327. package/dist/im/lark/card-handler.js.map +1 -1
  328. package/dist/im/lark/client.d.ts +8 -2
  329. package/dist/im/lark/client.d.ts.map +1 -1
  330. package/dist/im/lark/client.js +33 -31
  331. package/dist/im/lark/client.js.map +1 -1
  332. package/dist/im/lark/doc-comment.d.ts +18 -0
  333. package/dist/im/lark/doc-comment.d.ts.map +1 -1
  334. package/dist/im/lark/doc-comment.js +119 -5
  335. package/dist/im/lark/doc-comment.js.map +1 -1
  336. package/dist/im/lark/event-dispatcher.d.ts +18 -2
  337. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  338. package/dist/im/lark/event-dispatcher.js +41 -19
  339. package/dist/im/lark/event-dispatcher.js.map +1 -1
  340. package/dist/im/lark/merge-forward.d.ts.map +1 -1
  341. package/dist/im/lark/merge-forward.js +7 -2
  342. package/dist/im/lark/merge-forward.js.map +1 -1
  343. package/dist/im/lark/message-parser.d.ts.map +1 -1
  344. package/dist/im/lark/message-parser.js +7 -0
  345. package/dist/im/lark/message-parser.js.map +1 -1
  346. package/dist/im/lark/overview-card.d.ts +1 -2
  347. package/dist/im/lark/overview-card.d.ts.map +1 -1
  348. package/dist/im/lark/overview-card.js +1 -48
  349. package/dist/im/lark/overview-card.js.map +1 -1
  350. package/dist/im/lark/v3-blocked-card-handler.d.ts.map +1 -1
  351. package/dist/im/lark/v3-blocked-card-handler.js +5 -5
  352. package/dist/im/lark/v3-blocked-card-handler.js.map +1 -1
  353. package/dist/im/lark/v3-blocked-card.d.ts +1 -0
  354. package/dist/im/lark/v3-blocked-card.d.ts.map +1 -1
  355. package/dist/im/lark/v3-blocked-card.js +19 -2
  356. package/dist/im/lark/v3-blocked-card.js.map +1 -1
  357. package/dist/im/lark/v3-distillation-card-handler.d.ts +12 -0
  358. package/dist/im/lark/v3-distillation-card-handler.d.ts.map +1 -0
  359. package/dist/im/lark/v3-distillation-card-handler.js +107 -0
  360. package/dist/im/lark/v3-distillation-card-handler.js.map +1 -0
  361. package/dist/im/lark/v3-distillation-card.d.ts +42 -0
  362. package/dist/im/lark/v3-distillation-card.d.ts.map +1 -0
  363. package/dist/im/lark/v3-distillation-card.js +206 -0
  364. package/dist/im/lark/v3-distillation-card.js.map +1 -0
  365. package/dist/im/lark/v3-gate-card-handler.d.ts +1 -1
  366. package/dist/im/lark/v3-gate-card-handler.d.ts.map +1 -1
  367. package/dist/im/lark/v3-gate-card-handler.js +6 -5
  368. package/dist/im/lark/v3-gate-card-handler.js.map +1 -1
  369. package/dist/im/lark/v3-gate-card.d.ts +8 -1
  370. package/dist/im/lark/v3-gate-card.d.ts.map +1 -1
  371. package/dist/im/lark/v3-gate-card.js +38 -7
  372. package/dist/im/lark/v3-gate-card.js.map +1 -1
  373. package/dist/im/lark/v3-loop-grant-card-handler.d.ts.map +1 -1
  374. package/dist/im/lark/v3-loop-grant-card-handler.js +3 -4
  375. package/dist/im/lark/v3-loop-grant-card-handler.js.map +1 -1
  376. package/dist/im/lark/v3-loop-grant-card.js +1 -1
  377. package/dist/im/lark/v3-loop-grant-card.js.map +1 -1
  378. package/dist/im/lark/v3-progress-card-manager.d.ts +79 -0
  379. package/dist/im/lark/v3-progress-card-manager.d.ts.map +1 -0
  380. package/dist/im/lark/v3-progress-card-manager.js +444 -0
  381. package/dist/im/lark/v3-progress-card-manager.js.map +1 -0
  382. package/dist/im/lark/v3-progress-card.d.ts +26 -0
  383. package/dist/im/lark/v3-progress-card.d.ts.map +1 -0
  384. package/dist/im/lark/v3-progress-card.js +211 -0
  385. package/dist/im/lark/v3-progress-card.js.map +1 -0
  386. package/dist/im/lark/v3-revisit-grant-card-handler.d.ts.map +1 -1
  387. package/dist/im/lark/v3-revisit-grant-card-handler.js +3 -4
  388. package/dist/im/lark/v3-revisit-grant-card-handler.js.map +1 -1
  389. package/dist/im/lark/v3-revisit-grant-card.js +1 -1
  390. package/dist/im/lark/v3-revisit-grant-card.js.map +1 -1
  391. package/dist/im/lark/v3-run-save-card-handler.d.ts +12 -0
  392. package/dist/im/lark/v3-run-save-card-handler.d.ts.map +1 -0
  393. package/dist/im/lark/v3-run-save-card-handler.js +160 -0
  394. package/dist/im/lark/v3-run-save-card-handler.js.map +1 -0
  395. package/dist/im/lark/v3-run-save-card.d.ts +38 -0
  396. package/dist/im/lark/v3-run-save-card.d.ts.map +1 -0
  397. package/dist/im/lark/v3-run-save-card.js +125 -0
  398. package/dist/im/lark/v3-run-save-card.js.map +1 -0
  399. package/dist/im/lark/v3-saved-workflow-command.d.ts +36 -0
  400. package/dist/im/lark/v3-saved-workflow-command.d.ts.map +1 -0
  401. package/dist/im/lark/v3-saved-workflow-command.js +186 -0
  402. package/dist/im/lark/v3-saved-workflow-command.js.map +1 -0
  403. package/dist/im/lark/v3-saved-workflow-handler.d.ts +89 -0
  404. package/dist/im/lark/v3-saved-workflow-handler.d.ts.map +1 -0
  405. package/dist/im/lark/v3-saved-workflow-handler.js +212 -0
  406. package/dist/im/lark/v3-saved-workflow-handler.js.map +1 -0
  407. package/dist/im/lark/workflow-slash-command.d.ts +7 -110
  408. package/dist/im/lark/workflow-slash-command.d.ts.map +1 -1
  409. package/dist/im/lark/workflow-slash-command.js +16 -230
  410. package/dist/im/lark/workflow-slash-command.js.map +1 -1
  411. package/dist/index-daemon.js +1 -1
  412. package/dist/index-daemon.js.map +1 -1
  413. package/dist/mir-runner.js +11 -15
  414. package/dist/mir-runner.js.map +1 -1
  415. package/dist/mira-runner.js +8 -12
  416. package/dist/mira-runner.js.map +1 -1
  417. package/dist/services/bridge-turn-queue.d.ts +29 -12
  418. package/dist/services/bridge-turn-queue.d.ts.map +1 -1
  419. package/dist/services/bridge-turn-queue.js +59 -23
  420. package/dist/services/bridge-turn-queue.js.map +1 -1
  421. package/dist/services/claude-transcript.d.ts +15 -0
  422. package/dist/services/claude-transcript.d.ts.map +1 -1
  423. package/dist/services/claude-transcript.js +24 -0
  424. package/dist/services/claude-transcript.js.map +1 -1
  425. package/dist/services/codex-bridge-queue.d.ts +13 -2
  426. package/dist/services/codex-bridge-queue.d.ts.map +1 -1
  427. package/dist/services/codex-bridge-queue.js +20 -3
  428. package/dist/services/codex-bridge-queue.js.map +1 -1
  429. package/dist/services/codex-transcript.d.ts +6 -0
  430. package/dist/services/codex-transcript.d.ts.map +1 -1
  431. package/dist/services/codex-transcript.js.map +1 -1
  432. package/dist/services/grok-transcript.d.ts +3 -2
  433. package/dist/services/grok-transcript.d.ts.map +1 -1
  434. package/dist/services/grok-transcript.js +73 -22
  435. package/dist/services/grok-transcript.js.map +1 -1
  436. package/dist/services/hook-runner.d.ts.map +1 -1
  437. package/dist/services/hook-runner.js +26 -3
  438. package/dist/services/hook-runner.js.map +1 -1
  439. package/dist/services/schedule-store.d.ts +37 -0
  440. package/dist/services/schedule-store.d.ts.map +1 -1
  441. package/dist/services/schedule-store.js +259 -141
  442. package/dist/services/schedule-store.js.map +1 -1
  443. package/dist/services/send-policy.d.ts +50 -0
  444. package/dist/services/send-policy.d.ts.map +1 -1
  445. package/dist/services/send-policy.js +65 -0
  446. package/dist/services/send-policy.js.map +1 -1
  447. package/dist/services/session-lifecycle-hooks.d.ts.map +1 -1
  448. package/dist/services/session-lifecycle-hooks.js +12 -0
  449. package/dist/services/session-lifecycle-hooks.js.map +1 -1
  450. package/dist/services/traex-transcript.d.ts +11 -2
  451. package/dist/services/traex-transcript.d.ts.map +1 -1
  452. package/dist/services/traex-transcript.js +142 -8
  453. package/dist/services/traex-transcript.js.map +1 -1
  454. package/dist/services/trigger-types.d.ts +6 -1
  455. package/dist/services/trigger-types.d.ts.map +1 -1
  456. package/dist/services/trigger-types.js.map +1 -1
  457. package/dist/services/turn-terminal-deduper.d.ts +14 -0
  458. package/dist/services/turn-terminal-deduper.d.ts.map +1 -0
  459. package/dist/services/turn-terminal-deduper.js +34 -0
  460. package/dist/services/turn-terminal-deduper.js.map +1 -0
  461. package/dist/services/vc-meeting-action-gate.d.ts +246 -0
  462. package/dist/services/vc-meeting-action-gate.d.ts.map +1 -0
  463. package/dist/services/vc-meeting-action-gate.js +756 -0
  464. package/dist/services/vc-meeting-action-gate.js.map +1 -0
  465. package/dist/services/vc-meeting-action-store.d.ts +222 -0
  466. package/dist/services/vc-meeting-action-store.d.ts.map +1 -0
  467. package/dist/services/vc-meeting-action-store.js +806 -0
  468. package/dist/services/vc-meeting-action-store.js.map +1 -0
  469. package/dist/services/vc-meeting-consumer-isolation.d.ts +27 -0
  470. package/dist/services/vc-meeting-consumer-isolation.d.ts.map +1 -0
  471. package/dist/services/vc-meeting-consumer-isolation.js +37 -0
  472. package/dist/services/vc-meeting-consumer-isolation.js.map +1 -0
  473. package/dist/services/vc-meeting-consumer-profile-bootstrap.d.ts +72 -0
  474. package/dist/services/vc-meeting-consumer-profile-bootstrap.d.ts.map +1 -0
  475. package/dist/services/vc-meeting-consumer-profile-bootstrap.js +343 -0
  476. package/dist/services/vc-meeting-consumer-profile-bootstrap.js.map +1 -0
  477. package/dist/services/vc-meeting-consumer-profile-store.d.ts +39 -0
  478. package/dist/services/vc-meeting-consumer-profile-store.d.ts.map +1 -0
  479. package/dist/services/vc-meeting-consumer-profile-store.js +190 -0
  480. package/dist/services/vc-meeting-consumer-profile-store.js.map +1 -0
  481. package/dist/services/vc-meeting-daemon-auth.d.ts +29 -0
  482. package/dist/services/vc-meeting-daemon-auth.d.ts.map +1 -0
  483. package/dist/services/vc-meeting-daemon-auth.js +117 -0
  484. package/dist/services/vc-meeting-daemon-auth.js.map +1 -0
  485. package/dist/services/vc-meeting-delivery-feed.d.ts +52 -0
  486. package/dist/services/vc-meeting-delivery-feed.d.ts.map +1 -0
  487. package/dist/services/vc-meeting-delivery-feed.js +231 -0
  488. package/dist/services/vc-meeting-delivery-feed.js.map +1 -0
  489. package/dist/services/vc-meeting-delivery-hub-store.d.ts +213 -0
  490. package/dist/services/vc-meeting-delivery-hub-store.d.ts.map +1 -0
  491. package/dist/services/vc-meeting-delivery-hub-store.js +1230 -0
  492. package/dist/services/vc-meeting-delivery-hub-store.js.map +1 -0
  493. package/dist/services/vc-meeting-delivery-protocol.d.ts +159 -0
  494. package/dist/services/vc-meeting-delivery-protocol.d.ts.map +1 -0
  495. package/dist/services/vc-meeting-delivery-protocol.js +443 -0
  496. package/dist/services/vc-meeting-delivery-protocol.js.map +1 -0
  497. package/dist/services/vc-meeting-delivery-receiver.d.ts +80 -0
  498. package/dist/services/vc-meeting-delivery-receiver.d.ts.map +1 -0
  499. package/dist/services/vc-meeting-delivery-receiver.js +543 -0
  500. package/dist/services/vc-meeting-delivery-receiver.js.map +1 -0
  501. package/dist/services/vc-meeting-delivery-store.d.ts +339 -0
  502. package/dist/services/vc-meeting-delivery-store.d.ts.map +1 -0
  503. package/dist/services/vc-meeting-delivery-store.js +1024 -0
  504. package/dist/services/vc-meeting-delivery-store.js.map +1 -0
  505. package/dist/services/vc-meeting-feed-metadata-store.d.ts +58 -0
  506. package/dist/services/vc-meeting-feed-metadata-store.d.ts.map +1 -0
  507. package/dist/services/vc-meeting-feed-metadata-store.js +356 -0
  508. package/dist/services/vc-meeting-feed-metadata-store.js.map +1 -0
  509. package/dist/services/vc-meeting-im-reply.d.ts +55 -0
  510. package/dist/services/vc-meeting-im-reply.d.ts.map +1 -0
  511. package/dist/services/vc-meeting-im-reply.js +279 -0
  512. package/dist/services/vc-meeting-im-reply.js.map +1 -0
  513. package/dist/services/vc-meeting-im-routing.d.ts +131 -0
  514. package/dist/services/vc-meeting-im-routing.d.ts.map +1 -0
  515. package/dist/services/vc-meeting-im-routing.js +449 -0
  516. package/dist/services/vc-meeting-im-routing.js.map +1 -0
  517. package/dist/services/vc-meeting-listener-message-store.d.ts +23 -0
  518. package/dist/services/vc-meeting-listener-message-store.d.ts.map +1 -0
  519. package/dist/services/vc-meeting-listener-message-store.js +142 -0
  520. package/dist/services/vc-meeting-listener-message-store.js.map +1 -0
  521. package/dist/services/vc-meeting-member-policy.d.ts +37 -0
  522. package/dist/services/vc-meeting-member-policy.d.ts.map +1 -0
  523. package/dist/services/vc-meeting-member-policy.js +102 -0
  524. package/dist/services/vc-meeting-member-policy.js.map +1 -0
  525. package/dist/services/vc-meeting-profile-instructions.d.ts +22 -0
  526. package/dist/services/vc-meeting-profile-instructions.d.ts.map +1 -0
  527. package/dist/services/vc-meeting-profile-instructions.js +64 -0
  528. package/dist/services/vc-meeting-profile-instructions.js.map +1 -0
  529. package/dist/services/vc-meeting-runtime-store.d.ts +63 -0
  530. package/dist/services/vc-meeting-runtime-store.d.ts.map +1 -1
  531. package/dist/services/vc-meeting-runtime-store.js +605 -116
  532. package/dist/services/vc-meeting-runtime-store.js.map +1 -1
  533. package/dist/services/vc-meeting-send-policy.d.ts +89 -0
  534. package/dist/services/vc-meeting-send-policy.d.ts.map +1 -0
  535. package/dist/services/vc-meeting-send-policy.js +150 -0
  536. package/dist/services/vc-meeting-send-policy.js.map +1 -0
  537. package/dist/services/vc-meeting-terminal-reconciler.d.ts +93 -0
  538. package/dist/services/vc-meeting-terminal-reconciler.d.ts.map +1 -0
  539. package/dist/services/vc-meeting-terminal-reconciler.js +198 -0
  540. package/dist/services/vc-meeting-terminal-reconciler.js.map +1 -0
  541. package/dist/setup/open-platform-automation.d.ts +2 -0
  542. package/dist/setup/open-platform-automation.d.ts.map +1 -1
  543. package/dist/setup/open-platform-automation.js +1 -0
  544. package/dist/setup/open-platform-automation.js.map +1 -1
  545. package/dist/setup/open-platform-outcome.d.ts +50 -0
  546. package/dist/setup/open-platform-outcome.d.ts.map +1 -0
  547. package/dist/setup/open-platform-outcome.js +56 -0
  548. package/dist/setup/open-platform-outcome.js.map +1 -0
  549. package/dist/setup/setup-args.d.ts +7 -2
  550. package/dist/setup/setup-args.d.ts.map +1 -1
  551. package/dist/setup/setup-args.js +21 -5
  552. package/dist/setup/setup-args.js.map +1 -1
  553. package/dist/skills/definitions.d.ts.map +1 -1
  554. package/dist/skills/definitions.js +66 -54
  555. package/dist/skills/definitions.js.map +1 -1
  556. package/dist/skills/injection-mode.d.ts +5 -4
  557. package/dist/skills/injection-mode.d.ts.map +1 -1
  558. package/dist/skills/injection-mode.js +19 -10
  559. package/dist/skills/injection-mode.js.map +1 -1
  560. package/dist/types.d.ts +187 -0
  561. package/dist/types.d.ts.map +1 -1
  562. package/dist/utils/canonical-input-hash.d.ts +5 -0
  563. package/dist/utils/canonical-input-hash.d.ts.map +1 -0
  564. package/dist/utils/canonical-input-hash.js +43 -0
  565. package/dist/utils/canonical-input-hash.js.map +1 -0
  566. package/dist/utils/canonical-json.d.ts +10 -0
  567. package/dist/utils/canonical-json.d.ts.map +1 -0
  568. package/dist/utils/canonical-json.js +23 -0
  569. package/dist/utils/canonical-json.js.map +1 -0
  570. package/dist/utils/child-env.d.ts +1 -1
  571. package/dist/utils/child-env.d.ts.map +1 -1
  572. package/dist/utils/child-env.js +8 -0
  573. package/dist/utils/child-env.js.map +1 -1
  574. package/dist/utils/daemon-discovery.d.ts +4 -0
  575. package/dist/utils/daemon-discovery.d.ts.map +1 -1
  576. package/dist/utils/daemon-discovery.js +12 -6
  577. package/dist/utils/daemon-discovery.js.map +1 -1
  578. package/dist/utils/file-lock.d.ts.map +1 -1
  579. package/dist/utils/file-lock.js +696 -102
  580. package/dist/utils/file-lock.js.map +1 -1
  581. package/dist/utils/fs-durability.d.ts +20 -0
  582. package/dist/utils/fs-durability.d.ts.map +1 -0
  583. package/dist/utils/fs-durability.js +84 -0
  584. package/dist/utils/fs-durability.js.map +1 -0
  585. package/dist/utils/global-install.d.ts +5 -2
  586. package/dist/utils/global-install.d.ts.map +1 -1
  587. package/dist/utils/global-install.js +22 -4
  588. package/dist/utils/global-install.js.map +1 -1
  589. package/dist/utils/herdr-web-history.d.ts +12 -0
  590. package/dist/utils/herdr-web-history.d.ts.map +1 -0
  591. package/dist/utils/herdr-web-history.js +74 -0
  592. package/dist/utils/herdr-web-history.js.map +1 -0
  593. package/dist/utils/herdr-web-terminal-binding.d.ts +39 -0
  594. package/dist/utils/herdr-web-terminal-binding.d.ts.map +1 -0
  595. package/dist/utils/herdr-web-terminal-binding.js +56 -0
  596. package/dist/utils/herdr-web-terminal-binding.js.map +1 -0
  597. package/dist/utils/pending-input-queue.d.ts +30 -1
  598. package/dist/utils/pending-input-queue.d.ts.map +1 -1
  599. package/dist/utils/pending-input-queue.js +45 -5
  600. package/dist/utils/pending-input-queue.js.map +1 -1
  601. package/dist/vc-agent/cards.d.ts +49 -4
  602. package/dist/vc-agent/cards.d.ts.map +1 -1
  603. package/dist/vc-agent/cards.js +460 -70
  604. package/dist/vc-agent/cards.js.map +1 -1
  605. package/dist/vc-agent/polling-source.d.ts +6 -1
  606. package/dist/vc-agent/polling-source.d.ts.map +1 -1
  607. package/dist/vc-agent/polling-source.js +17 -4
  608. package/dist/vc-agent/polling-source.js.map +1 -1
  609. package/dist/worker.js +1333 -283
  610. package/dist/worker.js.map +1 -1
  611. package/dist/workflows/definition.d.ts +28 -35
  612. package/dist/workflows/definition.d.ts.map +1 -1
  613. package/dist/workflows/definition.js +7 -24
  614. package/dist/workflows/definition.js.map +1 -1
  615. package/dist/workflows/events/payloads.d.ts +28 -28
  616. package/dist/workflows/events/replay.d.ts +5 -0
  617. package/dist/workflows/events/replay.d.ts.map +1 -1
  618. package/dist/workflows/events/replay.js +6 -1
  619. package/dist/workflows/events/replay.js.map +1 -1
  620. package/dist/workflows/events/schema.d.ts +663 -663
  621. package/dist/workflows/events/schema.d.ts.map +1 -1
  622. package/dist/workflows/events/schema.js +1 -4
  623. package/dist/workflows/events/schema.js.map +1 -1
  624. package/dist/workflows/hostExecutors/botmux-schedule.d.ts +5 -4
  625. package/dist/workflows/hostExecutors/botmux-schedule.d.ts.map +1 -1
  626. package/dist/workflows/hostExecutors/botmux-schedule.js +80 -35
  627. package/dist/workflows/hostExecutors/botmux-schedule.js.map +1 -1
  628. package/dist/workflows/hostExecutors/feishu-im.d.ts +3 -3
  629. package/dist/workflows/hostExecutors/feishu-im.d.ts.map +1 -1
  630. package/dist/workflows/hostExecutors/feishu-im.js +3 -3
  631. package/dist/workflows/hostExecutors/feishu-im.js.map +1 -1
  632. package/dist/workflows/hostExecutors/feishu-reply.d.ts +1 -1
  633. package/dist/workflows/hostExecutors/feishu-reply.d.ts.map +1 -1
  634. package/dist/workflows/hostExecutors/feishu-reply.js +1 -1
  635. package/dist/workflows/hostExecutors/feishu-reply.js.map +1 -1
  636. package/dist/workflows/hostExecutors/feishu-send.d.ts +1 -1
  637. package/dist/workflows/hostExecutors/feishu-send.d.ts.map +1 -1
  638. package/dist/workflows/hostExecutors/feishu-send.js +1 -1
  639. package/dist/workflows/hostExecutors/feishu-send.js.map +1 -1
  640. package/dist/workflows/hostExecutors/index.d.ts +0 -1
  641. package/dist/workflows/hostExecutors/index.d.ts.map +1 -1
  642. package/dist/workflows/hostExecutors/index.js +0 -1
  643. package/dist/workflows/hostExecutors/index.js.map +1 -1
  644. package/dist/workflows/hostExecutors/registry.d.ts +1 -1
  645. package/dist/workflows/hostExecutors/registry.d.ts.map +1 -1
  646. package/dist/workflows/hostExecutors/types.d.ts +21 -29
  647. package/dist/workflows/hostExecutors/types.d.ts.map +1 -1
  648. package/dist/workflows/migration/v2-definition-paths.d.ts +3 -0
  649. package/dist/workflows/migration/v2-definition-paths.d.ts.map +1 -0
  650. package/dist/workflows/migration/v2-definition-paths.js +10 -0
  651. package/dist/workflows/migration/v2-definition-paths.js.map +1 -0
  652. package/dist/workflows/migration/v2-ledger.d.ts +138 -0
  653. package/dist/workflows/migration/v2-ledger.d.ts.map +1 -0
  654. package/dist/workflows/migration/v2-ledger.js +599 -0
  655. package/dist/workflows/migration/v2-ledger.js.map +1 -0
  656. package/dist/workflows/migration/v2-migration-service.d.ts +40 -0
  657. package/dist/workflows/migration/v2-migration-service.d.ts.map +1 -0
  658. package/dist/workflows/migration/v2-migration-service.js +454 -0
  659. package/dist/workflows/migration/v2-migration-service.js.map +1 -0
  660. package/dist/workflows/migration/v2-read-only-ids.d.ts +7 -0
  661. package/dist/workflows/migration/v2-read-only-ids.d.ts.map +1 -0
  662. package/dist/workflows/migration/v2-read-only-ids.js +9 -0
  663. package/dist/workflows/migration/v2-read-only-ids.js.map +1 -0
  664. package/dist/workflows/migration/v2-run-archive-schema.d.ts +976 -0
  665. package/dist/workflows/migration/v2-run-archive-schema.d.ts.map +1 -0
  666. package/dist/workflows/migration/v2-run-archive-schema.js +113 -0
  667. package/dist/workflows/migration/v2-run-archive-schema.js.map +1 -0
  668. package/dist/workflows/migration/v2-run-archive.d.ts +92 -0
  669. package/dist/workflows/migration/v2-run-archive.d.ts.map +1 -0
  670. package/dist/workflows/migration/v2-run-archive.js +1220 -0
  671. package/dist/workflows/migration/v2-run-archive.js.map +1 -0
  672. package/dist/workflows/migration/v2-scanner.d.ts +22 -0
  673. package/dist/workflows/migration/v2-scanner.d.ts.map +1 -0
  674. package/dist/workflows/migration/v2-scanner.js +114 -0
  675. package/dist/workflows/migration/v2-scanner.js.map +1 -0
  676. package/dist/workflows/migration/v2-to-v3.d.ts +32 -0
  677. package/dist/workflows/migration/v2-to-v3.d.ts.map +1 -0
  678. package/dist/workflows/migration/v2-to-v3.js +494 -0
  679. package/dist/workflows/migration/v2-to-v3.js.map +1 -0
  680. package/dist/workflows/ops-projection.d.ts.map +1 -1
  681. package/dist/workflows/ops-projection.js +4 -3
  682. package/dist/workflows/ops-projection.js.map +1 -1
  683. package/dist/workflows/shared/idempotency-key.d.ts +19 -0
  684. package/dist/workflows/shared/idempotency-key.d.ts.map +1 -0
  685. package/dist/workflows/shared/idempotency-key.js +23 -0
  686. package/dist/workflows/shared/idempotency-key.js.map +1 -0
  687. package/dist/workflows/{params.d.ts → shared/params.d.ts} +42 -6
  688. package/dist/workflows/shared/params.d.ts.map +1 -0
  689. package/dist/workflows/{params.js → shared/params.js} +91 -4
  690. package/dist/workflows/shared/params.js.map +1 -0
  691. package/dist/workflows/shared/provider-reconciler.d.ts +47 -0
  692. package/dist/workflows/shared/provider-reconciler.d.ts.map +1 -0
  693. package/dist/workflows/shared/provider-reconciler.js +17 -0
  694. package/dist/workflows/shared/provider-reconciler.js.map +1 -0
  695. package/dist/workflows/{spawn-policy.d.ts → shared/sandbox-policy.d.ts} +2 -2
  696. package/dist/workflows/shared/sandbox-policy.d.ts.map +1 -0
  697. package/dist/workflows/{spawn-policy.js → shared/sandbox-policy.js} +1 -1
  698. package/dist/workflows/shared/sandbox-policy.js.map +1 -0
  699. package/dist/workflows/shared/worker-process.d.ts +29 -0
  700. package/dist/workflows/shared/worker-process.d.ts.map +1 -0
  701. package/dist/workflows/shared/worker-process.js +56 -0
  702. package/dist/workflows/shared/worker-process.js.map +1 -0
  703. package/dist/workflows/v3/architect.d.ts.map +1 -1
  704. package/dist/workflows/v3/architect.js +16 -4
  705. package/dist/workflows/v3/architect.js.map +1 -1
  706. package/dist/workflows/v3/attempt-ledger.d.ts +23 -0
  707. package/dist/workflows/v3/attempt-ledger.d.ts.map +1 -0
  708. package/dist/workflows/v3/attempt-ledger.js +74 -0
  709. package/dist/workflows/v3/attempt-ledger.js.map +1 -0
  710. package/dist/workflows/v3/bot-resolve.d.ts +19 -1
  711. package/dist/workflows/v3/bot-resolve.d.ts.map +1 -1
  712. package/dist/workflows/v3/bot-resolve.js +129 -3
  713. package/dist/workflows/v3/bot-resolve.js.map +1 -1
  714. package/dist/workflows/v3/cli-daemon-command-authority.d.ts +59 -0
  715. package/dist/workflows/v3/cli-daemon-command-authority.d.ts.map +1 -0
  716. package/dist/workflows/v3/cli-daemon-command-authority.js +175 -0
  717. package/dist/workflows/v3/cli-daemon-command-authority.js.map +1 -0
  718. package/dist/workflows/v3/cli-run.d.ts +27 -0
  719. package/dist/workflows/v3/cli-run.d.ts.map +1 -1
  720. package/dist/workflows/v3/cli-run.js +113 -29
  721. package/dist/workflows/v3/cli-run.js.map +1 -1
  722. package/dist/workflows/v3/contract.d.ts +16 -16
  723. package/dist/workflows/v3/contract.d.ts.map +1 -1
  724. package/dist/workflows/v3/contract.js +15 -11
  725. package/dist/workflows/v3/contract.js.map +1 -1
  726. package/dist/workflows/v3/daemon-ipc-auth.d.ts +113 -0
  727. package/dist/workflows/v3/daemon-ipc-auth.d.ts.map +1 -0
  728. package/dist/workflows/v3/daemon-ipc-auth.js +294 -0
  729. package/dist/workflows/v3/daemon-ipc-auth.js.map +1 -0
  730. package/dist/workflows/v3/daemon-ipc-body.d.ts +30 -0
  731. package/dist/workflows/v3/daemon-ipc-body.d.ts.map +1 -0
  732. package/dist/workflows/v3/daemon-ipc-body.js +65 -0
  733. package/dist/workflows/v3/daemon-ipc-body.js.map +1 -0
  734. package/dist/workflows/v3/daemon-ipc-client.d.ts +32 -0
  735. package/dist/workflows/v3/daemon-ipc-client.d.ts.map +1 -0
  736. package/dist/workflows/v3/daemon-ipc-client.js +74 -0
  737. package/dist/workflows/v3/daemon-ipc-client.js.map +1 -0
  738. package/dist/workflows/v3/daemon-run.d.ts +87 -2
  739. package/dist/workflows/v3/daemon-run.d.ts.map +1 -1
  740. package/dist/workflows/v3/daemon-run.js +863 -68
  741. package/dist/workflows/v3/daemon-run.js.map +1 -1
  742. package/dist/workflows/v3/dag.d.ts +22 -8
  743. package/dist/workflows/v3/dag.d.ts.map +1 -1
  744. package/dist/workflows/v3/dag.js +214 -22
  745. package/dist/workflows/v3/dag.js.map +1 -1
  746. package/dist/workflows/v3/distillation-compiler.d.ts +36 -0
  747. package/dist/workflows/v3/distillation-compiler.d.ts.map +1 -0
  748. package/dist/workflows/v3/distillation-compiler.js +704 -0
  749. package/dist/workflows/v3/distillation-compiler.js.map +1 -0
  750. package/dist/workflows/v3/distillation-private-root.d.ts +10 -0
  751. package/dist/workflows/v3/distillation-private-root.d.ts.map +1 -0
  752. package/dist/workflows/v3/distillation-private-root.js +34 -0
  753. package/dist/workflows/v3/distillation-private-root.js.map +1 -0
  754. package/dist/workflows/v3/distillation-runner.d.ts +64 -0
  755. package/dist/workflows/v3/distillation-runner.d.ts.map +1 -0
  756. package/dist/workflows/v3/distillation-runner.js +1028 -0
  757. package/dist/workflows/v3/distillation-runner.js.map +1 -0
  758. package/dist/workflows/v3/distillation-schema.d.ts +63 -0
  759. package/dist/workflows/v3/distillation-schema.d.ts.map +1 -0
  760. package/dist/workflows/v3/distillation-schema.js +184 -0
  761. package/dist/workflows/v3/distillation-schema.js.map +1 -0
  762. package/dist/workflows/v3/distillation-service.d.ts +78 -0
  763. package/dist/workflows/v3/distillation-service.d.ts.map +1 -0
  764. package/dist/workflows/v3/distillation-service.js +416 -0
  765. package/dist/workflows/v3/distillation-service.js.map +1 -0
  766. package/dist/workflows/v3/distillation-source.d.ts +35 -0
  767. package/dist/workflows/v3/distillation-source.d.ts.map +1 -0
  768. package/dist/workflows/v3/distillation-source.js +118 -0
  769. package/dist/workflows/v3/distillation-source.js.map +1 -0
  770. package/dist/workflows/v3/distillation-store.d.ts +185 -0
  771. package/dist/workflows/v3/distillation-store.d.ts.map +1 -0
  772. package/dist/workflows/v3/distillation-store.js +1127 -0
  773. package/dist/workflows/v3/distillation-store.js.map +1 -0
  774. package/dist/workflows/v3/drive-lease.d.ts +7 -0
  775. package/dist/workflows/v3/drive-lease.d.ts.map +1 -0
  776. package/dist/workflows/v3/drive-lease.js +10 -0
  777. package/dist/workflows/v3/drive-lease.js.map +1 -0
  778. package/dist/workflows/v3/ephemeral-pool.d.ts +1 -1
  779. package/dist/workflows/v3/ephemeral-pool.d.ts.map +1 -1
  780. package/dist/workflows/v3/ephemeral-pool.js +222 -37
  781. package/dist/workflows/v3/ephemeral-pool.js.map +1 -1
  782. package/dist/workflows/v3/grill-state.d.ts +18 -3
  783. package/dist/workflows/v3/grill-state.d.ts.map +1 -1
  784. package/dist/workflows/v3/grill-state.js +22 -5
  785. package/dist/workflows/v3/grill-state.js.map +1 -1
  786. package/dist/workflows/v3/host-bindings.d.ts +40 -0
  787. package/dist/workflows/v3/host-bindings.d.ts.map +1 -0
  788. package/dist/workflows/v3/host-bindings.js +272 -0
  789. package/dist/workflows/v3/host-bindings.js.map +1 -0
  790. package/dist/workflows/v3/host-effect-ledger.d.ts +22 -0
  791. package/dist/workflows/v3/host-effect-ledger.d.ts.map +1 -0
  792. package/dist/workflows/v3/host-effect-ledger.js +110 -0
  793. package/dist/workflows/v3/host-effect-ledger.js.map +1 -0
  794. package/dist/workflows/v3/host-execution.d.ts +142 -0
  795. package/dist/workflows/v3/host-execution.d.ts.map +1 -0
  796. package/dist/workflows/v3/host-execution.js +424 -0
  797. package/dist/workflows/v3/host-execution.js.map +1 -0
  798. package/dist/workflows/v3/host.d.ts +46 -10
  799. package/dist/workflows/v3/host.d.ts.map +1 -1
  800. package/dist/workflows/v3/host.js +419 -140
  801. package/dist/workflows/v3/host.js.map +1 -1
  802. package/dist/workflows/v3/human-gate.d.ts +19 -1
  803. package/dist/workflows/v3/human-gate.d.ts.map +1 -1
  804. package/dist/workflows/v3/human-gate.js +145 -19
  805. package/dist/workflows/v3/human-gate.js.map +1 -1
  806. package/dist/workflows/v3/journal.d.ts +125 -7
  807. package/dist/workflows/v3/journal.d.ts.map +1 -1
  808. package/dist/workflows/v3/journal.js +154 -38
  809. package/dist/workflows/v3/journal.js.map +1 -1
  810. package/dist/workflows/v3/library-materialize.d.ts +88 -0
  811. package/dist/workflows/v3/library-materialize.d.ts.map +1 -0
  812. package/dist/workflows/v3/library-materialize.js +294 -0
  813. package/dist/workflows/v3/library-materialize.js.map +1 -0
  814. package/dist/workflows/v3/library-schema.d.ts +138 -0
  815. package/dist/workflows/v3/library-schema.d.ts.map +1 -0
  816. package/dist/workflows/v3/library-schema.js +573 -0
  817. package/dist/workflows/v3/library-schema.js.map +1 -0
  818. package/dist/workflows/v3/library-service.d.ts +126 -0
  819. package/dist/workflows/v3/library-service.d.ts.map +1 -0
  820. package/dist/workflows/v3/library-service.js +542 -0
  821. package/dist/workflows/v3/library-service.js.map +1 -0
  822. package/dist/workflows/v3/library-store.d.ts +124 -0
  823. package/dist/workflows/v3/library-store.d.ts.map +1 -0
  824. package/dist/workflows/v3/library-store.js +665 -0
  825. package/dist/workflows/v3/library-store.js.map +1 -0
  826. package/dist/workflows/v3/orchestrator.d.ts +9 -1
  827. package/dist/workflows/v3/orchestrator.d.ts.map +1 -1
  828. package/dist/workflows/v3/orchestrator.js +2 -1
  829. package/dist/workflows/v3/orchestrator.js.map +1 -1
  830. package/dist/workflows/v3/progress-projection.d.ts +99 -0
  831. package/dist/workflows/v3/progress-projection.d.ts.map +1 -0
  832. package/dist/workflows/v3/progress-projection.js +197 -0
  833. package/dist/workflows/v3/progress-projection.js.map +1 -0
  834. package/dist/workflows/v3/run-envelope.d.ts +1279 -0
  835. package/dist/workflows/v3/run-envelope.d.ts.map +1 -0
  836. package/dist/workflows/v3/run-envelope.js +681 -0
  837. package/dist/workflows/v3/run-envelope.js.map +1 -0
  838. package/dist/workflows/v3/runtime.d.ts +39 -7
  839. package/dist/workflows/v3/runtime.d.ts.map +1 -1
  840. package/dist/workflows/v3/runtime.js +1992 -100
  841. package/dist/workflows/v3/runtime.js.map +1 -1
  842. package/dist/workflows/v3/session-relay-client.d.ts +61 -0
  843. package/dist/workflows/v3/session-relay-client.d.ts.map +1 -0
  844. package/dist/workflows/v3/session-relay-client.js +92 -0
  845. package/dist/workflows/v3/session-relay-client.js.map +1 -0
  846. package/dist/workflows/v3/session-relay.d.ts +66 -0
  847. package/dist/workflows/v3/session-relay.d.ts.map +1 -0
  848. package/dist/workflows/v3/session-relay.js +157 -0
  849. package/dist/workflows/v3/session-relay.js.map +1 -0
  850. package/dist/workflows/v3/start-intent.d.ts +8 -0
  851. package/dist/workflows/v3/start-intent.d.ts.map +1 -0
  852. package/dist/workflows/v3/start-intent.js +34 -0
  853. package/dist/workflows/v3/start-intent.js.map +1 -0
  854. package/dist/workflows/v3/state.d.ts +11 -2
  855. package/dist/workflows/v3/state.d.ts.map +1 -1
  856. package/dist/workflows/v3/state.js +182 -10
  857. package/dist/workflows/v3/state.js.map +1 -1
  858. package/dist/workflows/v3/template-bindings.d.ts +22 -0
  859. package/dist/workflows/v3/template-bindings.d.ts.map +1 -0
  860. package/dist/workflows/v3/template-bindings.js +204 -0
  861. package/dist/workflows/v3/template-bindings.js.map +1 -0
  862. package/dist/workflows/v3/worker-fence.d.ts +158 -0
  863. package/dist/workflows/v3/worker-fence.d.ts.map +1 -0
  864. package/dist/workflows/v3/worker-fence.js +685 -0
  865. package/dist/workflows/v3/worker-fence.js.map +1 -0
  866. package/package.json +3 -2
  867. package/dist/core/dashboard-command/workflows.d.ts +0 -20
  868. package/dist/core/dashboard-command/workflows.d.ts.map +0 -1
  869. package/dist/core/dashboard-command/workflows.js +0 -45
  870. package/dist/core/dashboard-command/workflows.js.map +0 -1
  871. package/dist/dashboard/web/legacy-workflow-link.d.ts +0 -2
  872. package/dist/dashboard/web/legacy-workflow-link.d.ts.map +0 -1
  873. package/dist/dashboard/web/legacy-workflow-link.js +0 -12
  874. package/dist/dashboard/web/legacy-workflow-link.js.map +0 -1
  875. package/dist/dashboard/web/legacy-workflow-page.d.ts +0 -3
  876. package/dist/dashboard/web/legacy-workflow-page.d.ts.map +0 -1
  877. package/dist/dashboard/web/legacy-workflow-page.js +0 -717
  878. package/dist/dashboard/web/legacy-workflow-page.js.map +0 -1
  879. package/dist/dashboard/web/workflow-version-switch.d.ts +0 -8
  880. package/dist/dashboard/web/workflow-version-switch.d.ts.map +0 -1
  881. package/dist/dashboard/web/workflow-version-switch.js +0 -9
  882. package/dist/dashboard/web/workflow-version-switch.js.map +0 -1
  883. package/dist/dashboard/web/workflows.d.ts +0 -273
  884. package/dist/dashboard/web/workflows.d.ts.map +0 -1
  885. package/dist/dashboard/web/workflows.js +0 -436
  886. package/dist/dashboard/web/workflows.js.map +0 -1
  887. package/dist/dashboard/workflow-api.d.ts +0 -30
  888. package/dist/dashboard/workflow-api.d.ts.map +0 -1
  889. package/dist/dashboard/workflow-api.js +0 -397
  890. package/dist/dashboard/workflow-api.js.map +0 -1
  891. package/dist/dashboard/workflow-card-model.d.ts +0 -127
  892. package/dist/dashboard/workflow-card-model.d.ts.map +0 -1
  893. package/dist/dashboard/workflow-card-model.js +0 -186
  894. package/dist/dashboard/workflow-card-model.js.map +0 -1
  895. package/dist/dashboard/workflows-action-helpers.d.ts +0 -105
  896. package/dist/dashboard/workflows-action-helpers.d.ts.map +0 -1
  897. package/dist/dashboard/workflows-action-helpers.js +0 -196
  898. package/dist/dashboard/workflows-action-helpers.js.map +0 -1
  899. package/dist/dashboard-web/chunks/bot-defaults-page-7T7DPOV6.js +0 -4
  900. package/dist/dashboard-web/chunks/chunk-IGXYYRYJ.js +0 -1
  901. package/dist/dashboard-web/chunks/chunk-ISOD7VDX.js +0 -1
  902. package/dist/dashboard-web/chunks/chunk-PNK3HQBI.js +0 -37
  903. package/dist/dashboard-web/chunks/chunk-SRIE4MLQ.js +0 -1
  904. package/dist/dashboard-web/chunks/connectors-page-UQ4QBE45.js +0 -1
  905. package/dist/dashboard-web/chunks/legacy-workflow-page-QMC7OZXH.js +0 -2
  906. package/dist/dashboard-web/chunks/sessions-page-NNZIDHZM.js +0 -3
  907. package/dist/dashboard-web/chunks/settings-page-LJIXX5LQ.js +0 -2
  908. package/dist/dashboard-web/chunks/v3-page-YLYVHGKD.js +0 -94
  909. package/dist/dashboard-web/terminal-replay.html +0 -227
  910. package/dist/im/lark/workflow-card-handler.d.ts +0 -51
  911. package/dist/im/lark/workflow-card-handler.d.ts.map +0 -1
  912. package/dist/im/lark/workflow-card-handler.js +0 -163
  913. package/dist/im/lark/workflow-card-handler.js.map +0 -1
  914. package/dist/im/lark/workflow-cards.d.ts +0 -47
  915. package/dist/im/lark/workflow-cards.d.ts.map +0 -1
  916. package/dist/im/lark/workflow-cards.js +0 -231
  917. package/dist/im/lark/workflow-cards.js.map +0 -1
  918. package/dist/im/lark/workflow-progress-card.d.ts +0 -81
  919. package/dist/im/lark/workflow-progress-card.d.ts.map +0 -1
  920. package/dist/im/lark/workflow-progress-card.js +0 -336
  921. package/dist/im/lark/workflow-progress-card.js.map +0 -1
  922. package/dist/im/lark/workflows-card.d.ts +0 -102
  923. package/dist/im/lark/workflows-card.d.ts.map +0 -1
  924. package/dist/im/lark/workflows-card.js +0 -737
  925. package/dist/im/lark/workflows-card.js.map +0 -1
  926. package/dist/workflows/attempt-resume.d.ts +0 -119
  927. package/dist/workflows/attempt-resume.d.ts.map +0 -1
  928. package/dist/workflows/attempt-resume.js +0 -392
  929. package/dist/workflows/attempt-resume.js.map +0 -1
  930. package/dist/workflows/blob.d.ts +0 -27
  931. package/dist/workflows/blob.d.ts.map +0 -1
  932. package/dist/workflows/blob.js +0 -40
  933. package/dist/workflows/blob.js.map +0 -1
  934. package/dist/workflows/cancel-run.d.ts +0 -45
  935. package/dist/workflows/cancel-run.d.ts.map +0 -1
  936. package/dist/workflows/cancel-run.js +0 -99
  937. package/dist/workflows/cancel-run.js.map +0 -1
  938. package/dist/workflows/cancel.d.ts +0 -111
  939. package/dist/workflows/cancel.d.ts.map +0 -1
  940. package/dist/workflows/cancel.js +0 -120
  941. package/dist/workflows/cancel.js.map +0 -1
  942. package/dist/workflows/catalog.d.ts +0 -60
  943. package/dist/workflows/catalog.d.ts.map +0 -1
  944. package/dist/workflows/catalog.js +0 -119
  945. package/dist/workflows/catalog.js.map +0 -1
  946. package/dist/workflows/cold-attach.d.ts +0 -30
  947. package/dist/workflows/cold-attach.d.ts.map +0 -1
  948. package/dist/workflows/cold-attach.js +0 -40
  949. package/dist/workflows/cold-attach.js.map +0 -1
  950. package/dist/workflows/cold-scan.d.ts +0 -21
  951. package/dist/workflows/cold-scan.d.ts.map +0 -1
  952. package/dist/workflows/cold-scan.js +0 -70
  953. package/dist/workflows/cold-scan.js.map +0 -1
  954. package/dist/workflows/daemon-spawn.d.ts +0 -119
  955. package/dist/workflows/daemon-spawn.d.ts.map +0 -1
  956. package/dist/workflows/daemon-spawn.js +0 -564
  957. package/dist/workflows/daemon-spawn.js.map +0 -1
  958. package/dist/workflows/effect-input.d.ts +0 -4
  959. package/dist/workflows/effect-input.d.ts.map +0 -1
  960. package/dist/workflows/effect-input.js +0 -19
  961. package/dist/workflows/effect-input.js.map +0 -1
  962. package/dist/workflows/events/append.d.ts +0 -77
  963. package/dist/workflows/events/append.d.ts.map +0 -1
  964. package/dist/workflows/events/append.js +0 -214
  965. package/dist/workflows/events/append.js.map +0 -1
  966. package/dist/workflows/events/idempotency.d.ts +0 -77
  967. package/dist/workflows/events/idempotency.d.ts.map +0 -1
  968. package/dist/workflows/events/idempotency.js +0 -116
  969. package/dist/workflows/events/idempotency.js.map +0 -1
  970. package/dist/workflows/events/index.d.ts +0 -7
  971. package/dist/workflows/events/index.d.ts.map +0 -1
  972. package/dist/workflows/events/index.js +0 -7
  973. package/dist/workflows/events/index.js.map +0 -1
  974. package/dist/workflows/fanout.d.ts +0 -36
  975. package/dist/workflows/fanout.d.ts.map +0 -1
  976. package/dist/workflows/fanout.js +0 -115
  977. package/dist/workflows/fanout.js.map +0 -1
  978. package/dist/workflows/hostExecutors/protocol.d.ts +0 -42
  979. package/dist/workflows/hostExecutors/protocol.d.ts.map +0 -1
  980. package/dist/workflows/hostExecutors/protocol.js +0 -181
  981. package/dist/workflows/hostExecutors/protocol.js.map +0 -1
  982. package/dist/workflows/loader.d.ts +0 -30
  983. package/dist/workflows/loader.d.ts.map +0 -1
  984. package/dist/workflows/loader.js +0 -84
  985. package/dist/workflows/loader.js.map +0 -1
  986. package/dist/workflows/loop.d.ts +0 -50
  987. package/dist/workflows/loop.d.ts.map +0 -1
  988. package/dist/workflows/loop.js +0 -408
  989. package/dist/workflows/loop.js.map +0 -1
  990. package/dist/workflows/orchestrator.d.ts +0 -171
  991. package/dist/workflows/orchestrator.d.ts.map +0 -1
  992. package/dist/workflows/orchestrator.js +0 -609
  993. package/dist/workflows/orchestrator.js.map +0 -1
  994. package/dist/workflows/output-binding.d.ts +0 -77
  995. package/dist/workflows/output-binding.d.ts.map +0 -1
  996. package/dist/workflows/output-binding.js +0 -329
  997. package/dist/workflows/output-binding.js.map +0 -1
  998. package/dist/workflows/params.d.ts.map +0 -1
  999. package/dist/workflows/params.js.map +0 -1
  1000. package/dist/workflows/resume.d.ts +0 -263
  1001. package/dist/workflows/resume.d.ts.map +0 -1
  1002. package/dist/workflows/resume.js +0 -808
  1003. package/dist/workflows/resume.js.map +0 -1
  1004. package/dist/workflows/run-id.d.ts +0 -2
  1005. package/dist/workflows/run-id.d.ts.map +0 -1
  1006. package/dist/workflows/run-id.js +0 -7
  1007. package/dist/workflows/run-id.js.map +0 -1
  1008. package/dist/workflows/run-init.d.ts +0 -48
  1009. package/dist/workflows/run-init.d.ts.map +0 -1
  1010. package/dist/workflows/run-init.js +0 -100
  1011. package/dist/workflows/run-init.js.map +0 -1
  1012. package/dist/workflows/runs-dir.d.ts +0 -4
  1013. package/dist/workflows/runs-dir.d.ts.map +0 -1
  1014. package/dist/workflows/runs-dir.js +0 -15
  1015. package/dist/workflows/runs-dir.js.map +0 -1
  1016. package/dist/workflows/runtime.d.ts +0 -215
  1017. package/dist/workflows/runtime.d.ts.map +0 -1
  1018. package/dist/workflows/runtime.js +0 -630
  1019. package/dist/workflows/runtime.js.map +0 -1
  1020. package/dist/workflows/spawn-bot.d.ts +0 -165
  1021. package/dist/workflows/spawn-bot.d.ts.map +0 -1
  1022. package/dist/workflows/spawn-bot.js +0 -215
  1023. package/dist/workflows/spawn-bot.js.map +0 -1
  1024. package/dist/workflows/spawn-policy.d.ts.map +0 -1
  1025. package/dist/workflows/spawn-policy.js.map +0 -1
  1026. package/dist/workflows/system.d.ts +0 -49
  1027. package/dist/workflows/system.d.ts.map +0 -1
  1028. package/dist/workflows/system.js +0 -48
  1029. package/dist/workflows/system.js.map +0 -1
  1030. package/dist/workflows/trigger-from-envelope.d.ts +0 -13
  1031. package/dist/workflows/trigger-from-envelope.d.ts.map +0 -1
  1032. package/dist/workflows/trigger-from-envelope.js +0 -68
  1033. package/dist/workflows/trigger-from-envelope.js.map +0 -1
  1034. package/dist/workflows/trigger-run.d.ts +0 -70
  1035. package/dist/workflows/trigger-run.d.ts.map +0 -1
  1036. package/dist/workflows/trigger-run.js +0 -88
  1037. package/dist/workflows/trigger-run.js.map +0 -1
  1038. package/dist/workflows/wait.d.ts +0 -141
  1039. package/dist/workflows/wait.d.ts.map +0 -1
  1040. package/dist/workflows/wait.js +0 -203
  1041. package/dist/workflows/wait.js.map +0 -1
package/dist/worker.js CHANGED
@@ -13,42 +13,46 @@
13
13
  * 7. On 'restart', kills CLI and re-spawns with --resume
14
14
  */
15
15
  import { randomBytes } from 'node:crypto';
16
- import { mkdirSync, writeFileSync, unlinkSync, existsSync, statSync, readdirSync, readlinkSync, readFileSync, realpathSync, copyFileSync, watch as fsWatch, createWriteStream } from 'node:fs';
16
+ import { mkdirSync, writeFileSync, unlinkSync, existsSync, statSync, readdirSync, readlinkSync, readFileSync, realpathSync, copyFileSync, watch as fsWatch, createWriteStream, openSync, closeSync, fstatSync, constants as fsConstants } from 'node:fs';
17
17
  import { atomicWriteFileSync } from './utils/atomic-write.js';
18
18
  import { join, basename } from 'node:path';
19
19
  import { homedir } from 'node:os';
20
20
  import { spawnSync } from 'node:child_process';
21
- import { evaluateReadIsolationGate, buildSeatbeltProfile, isolatedPaneReattachSafe, sendCredFilePath, botHomePath, buildV2DenyPaths, buildV2DenyRegexes, buildV2CarveOuts, buildCliExecutableReadCarveOuts, buildWriteSandboxRules, buildLinuxReadIsolationMasks, } from './adapters/cli/read-isolation.js';
21
+ import { evaluateReadIsolationGate, buildSeatbeltProfile, isolatedPaneReattachSafe, sendCredFilePath, botHomePath, buildV2DenyPaths, buildV2DenyRegexes, buildV2CarveOuts, buildReadIsolationProtectedWriteRules, buildCliExecutableReadCarveOuts, buildWriteSandboxRules, buildLinuxReadIsolationMasks, isolationPaneMarkerContent, } from './adapters/cli/read-isolation.js';
22
22
  import { killPersistentSession } from './core/persistent-backend.js';
23
+ import { readProcessStartIdentity } from './core/session-marker.js';
23
24
  import { drainTranscript, joinAssistantText, trailingAssistantText, findJsonlContainingFingerprint, findJsonlsContainingExactContent, findLatestJsonl, extractLastAssistantTurn, extractTurnStartText, splitTranscriptEventsByCutoff } from './services/claude-transcript.js';
24
25
  import { BridgeTurnQueue, makeFingerprint, normaliseForFingerprint } from './services/bridge-turn-queue.js';
25
26
  import { shouldSuppressBridgeEmit } from './services/bridge-fallback-gate.js';
26
27
  import { shouldReleaseFirstPromptTimeout, shouldWriteNow } from './utils/input-gate.js';
28
+ import { canStartInjectionFlush, shouldDeferUserFlush, shouldFlushInjectionsFirst } from './core/inject-queue-policy.js';
27
29
  import { stripAnsiForLog, tailChars } from './utils/crash-log.js';
28
30
  import { CodexUpdateDialogGuard } from './utils/codex-update-dialog.js';
29
31
  import { installStdioEpipeGuard, isIgnorableStreamError } from './utils/stdio-epipe-guard.js';
30
- import { mergeQueuedCliInput } from './utils/pending-input-queue.js';
32
+ import { mergeQueuedCliInput, pendingInputMayFlush, pendingInputAllowsTypeAhead, shouldDeferArgsBakedDurablePrompt, shouldStopPendingBatch, terminalReleasesDurableTurn, } from './utils/pending-input-queue.js';
31
33
  import { ReadyGate, shouldArmReadyGate } from './utils/ready-gate.js';
32
34
  import { shouldRunStartupCommandsOnSpawn, shouldDeferInitialPromptForStartup } from './core/startup-commands.js';
33
35
  import { sanitizePerBotEnv } from './core/per-bot-env.js';
36
+ import { evaluateVcMeetingManagedSend, } from './services/vc-meeting-send-policy.js';
37
+ import { TurnTerminalDeduper } from './services/turn-terminal-deduper.js';
34
38
  import { ensureGatewayEntry } from './core/plugins/mcp/gateway-installer.js';
35
39
  import { prepareCliPluginGeneration } from './core/plugins/cli-generation.js';
36
40
  import { loadBotConfigs } from './bot-registry.js';
37
41
  import { readGlobalConfig } from './global-config.js';
38
- import { resolveTerminalWriteForRequest } from './core/terminal-write-auth.js';
42
+ import { deriveTerminalViewToken, resolveTerminalAccessForRequest, safeTerminalTokenEqual, } from './core/terminal-write-auth.js';
39
43
  import { readPlatformBinding } from './platform/binding.js';
40
- import { loadPersistedToken } from './dashboard/auth.js';
44
+ import { loadDashboardSecret, loadPersistedToken } from './dashboard/auth.js';
41
45
  import { InflightInputTracker } from './core/inflight-input-tracker.js';
42
46
  import { shouldRunQuietRotation, evaluatePidResolverPullback, decideFingerprintSwitch, shouldHealAbsentBaseline, sessionIdFromJsonlPath, SESSION_ID_FILENAME_RE, } from './services/bridge-rotation-policy.js';
43
47
  import { CodexBridgeQueue } from './services/codex-bridge-queue.js';
44
- import { codexSessionIdFromRolloutPath, drainCodexRollout, findCodexRolloutBySessionId, findCodexRolloutByPid, splitCodexEventsByCutoff, extractLastCodexTurn } from './services/codex-transcript.js';
45
- import { findTraexRolloutBySessionId, findTraexRolloutByPid } from './services/traex-transcript.js';
48
+ import { drainCodexRollout, findCodexRolloutBySessionId, findCodexRolloutByPid, splitCodexEventsByCutoff, extractLastCodexTurn, codexSessionIdFromRolloutPath } from './services/codex-transcript.js';
49
+ import { drainTraexRollout, findTraexRolloutBySessionId, findTraexRolloutByPid } from './services/traex-transcript.js';
46
50
  import { cocoEventsPathForSession, drainCocoEvents, findCocoSessionByPid } from './services/coco-transcript.js';
47
51
  import { currentHermesStateOffset, drainHermesStateDb, resolveHermesStateDbPath } from './services/hermes-transcript.js';
48
52
  import { filterHermesEventsForBotmuxSession } from './services/hermes-session-filter.js';
49
53
  import { currentMtrSessionOffset, drainMtrSession, findLatestMtrSessionByDirectory, findMtrSessionById } from './services/mtr-transcript.js';
50
54
  import { drainPiTranscript } from './services/pi-transcript.js';
51
- import { drainGrokUpdates, grokSessionIdFromPath, } from './services/grok-transcript.js';
55
+ import { drainGrokUpdates, findGrokSessionByPid, grokSessionIdFromPath, } from './services/grok-transcript.js';
52
56
  import { resolveFileBridgePath } from './services/file-bridge-path.js';
53
57
  import { isStructuredBridgeAdoptIdleCli, isStructuredBridgeAdoptInputCli, isStructuredBridgeFallbackActive, } from './services/structured-bridge-clis.js';
54
58
  import { drainCursorTranscript, findCursorChatIdByPid, findCursorTranscriptByChatId, findCursorTranscriptByPid } from './services/cursor-transcript.js';
@@ -59,6 +63,7 @@ import { dirname } from 'node:path';
59
63
  import { createServer as createHttpServer } from 'node:http';
60
64
  import { WebSocketServer, WebSocket } from 'ws';
61
65
  import { listenWebTerminalWithFallback } from './utils/web-terminal-listen.js';
66
+ import { HerdrWebTerminalBinding } from './utils/herdr-web-terminal-binding.js';
62
67
  import { TERMINAL_FAVICON_DATA_URI } from './utils/terminal-favicon.js';
63
68
  import { t, setDefaultLocale } from './i18n/index.js';
64
69
  import { TerminalRenderer } from './utils/terminal-renderer.js';
@@ -87,6 +92,7 @@ import { ScreenAnalyzer } from './utils/screen-analyzer.js';
87
92
  import { captureToPng } from './utils/screenshot-renderer.js';
88
93
  import { snapshotToPng, snapshotToText, shouldCaptureScreen, isScreenSelfDriven } from './utils/transient-snapshot.js';
89
94
  import { chooseWebTerminalSeed } from './utils/web-terminal-seed.js';
95
+ import { mergeHerdrWebSnapshot, renderHerdrWebHistory, } from './utils/herdr-web-history.js';
90
96
  import { parseWorkerRequestUrl } from './utils/worker-http.js';
91
97
  import { detectCliUsageLimit, usageLimitStateKey } from './utils/cli-usage-limit.js';
92
98
  import { uploadImageBuffer } from './utils/lark-upload.js';
@@ -99,13 +105,20 @@ import { createHash } from 'node:crypto';
99
105
  import { installHook } from './adapters/hook-installer.js';
100
106
  import { hookCommandFor } from './adapters/hook-command.js';
101
107
  import { withCodexAppContext } from './utils/codex-app-context.js';
108
+ import { resolveCodexAppFinalTurnIdentity } from './adapters/cli/codex-app-turn.js';
109
+ import { RunnerControlDecoder } from './adapters/cli/runner-control-channel.js';
110
+ import { managedOriginCapabilityPath, RELAY_ORIGIN_CAPABILITY_BASENAME, replaceManagedOriginCapabilityFile, } from './core/managed-origin-capability.js';
102
111
  // ─── State ───────────────────────────────────────────────────────────────────
103
112
  let cliAdapter = null;
104
113
  let backend = null;
114
+ let intentionalRestartBackend = null;
105
115
  let cliPidMarker = null; // path to .botmux-cli-pids/<pid>
106
116
  let seatbeltProfilePath = null; // per-session Seatbelt .sb profile to rm at exit (external-wrapper read isolation)
107
117
  let sandboxStopWatcher = null; // stop fn for the sandbox outbox watcher
108
118
  let sandboxCleanup = null; // unmount overlays + rm the per-session sandbox tree
119
+ let sandboxRelayOutbox = null;
120
+ let sandboxRelayCapability = null;
121
+ let readIsolationOriginCapabilityFile = null;
109
122
  let sandboxTeardownDone = false; // guards the exit-time best-effort teardown from double-running / running on suspend-for-resume
110
123
  /** Counts consecutive in-worker restart cycles (see case 'restart'). Used by
111
124
  * the SECONDARY guard so an adapter whose checkResumeTargetExists misses
@@ -349,12 +362,23 @@ const wsClients = new Set();
349
362
  const authedClients = new WeakSet();
350
363
  /** Per-WS-client tmux/zellij attach PTYs. */
351
364
  const clientPtys = new Map();
365
+ /** Managed-Herdr viewers survive an in-worker /restart while backend changes. */
366
+ const herdrWebBindings = new Map();
352
367
  const writeToken = randomBytes(16).toString('hex');
368
+ // Standalone/test fallback. Production replaces this after init with a stable
369
+ // per-session HMAC derived from the host-only dashboard secret.
370
+ let viewToken = randomBytes(32).toString('base64url');
353
371
  // Active dashboard token, persisted by the dashboard process at this stable
354
372
  // path (mirrors dashboard.ts TOKEN_PATH). The platform proxy injects it as the
355
373
  // `botmux_dashboard_token` cookie on every request it fronts, so its presence
356
374
  // proves a request traversed the platform's authenticated front door.
357
375
  const DASHBOARD_TOKEN_PATH = join(homedir(), '.botmux', '.dashboard-token');
376
+ const DASHBOARD_SECRET_PATH = join(homedir(), '.botmux', '.dashboard-secret');
377
+ function refreshTerminalViewToken() {
378
+ const secret = loadDashboardSecret(DASHBOARD_SECRET_PATH);
379
+ if (secret && sessionId)
380
+ viewToken = deriveTerminalViewToken(secret, sessionId);
381
+ }
358
382
  /**
359
383
  * Resolve terminal write permission for one request. The platform-injected
360
384
  * `X-Botmux-Role` header is trusted only when this machine is bound to a central
@@ -369,8 +393,8 @@ const DASHBOARD_TOKEN_PATH = join(homedir(), '.botmux', '.dashboard-token');
369
393
  * `botmux bind`/unbind and dashboard token rotation are hot-reloaded without
370
394
  * restarting this worker, so a cached value would go stale.
371
395
  */
372
- function resolveTerminalWriteForReq(req, tokenMatches) {
373
- return resolveTerminalWriteForRequest(req.headers, tokenMatches, () => readPlatformBinding() !== null, () => loadPersistedToken(DASHBOARD_TOKEN_PATH));
396
+ function resolveTerminalAccessForReq(req, url) {
397
+ return resolveTerminalAccessForRequest(req.headers, safeTerminalTokenEqual(url.searchParams.get('token'), writeToken), safeTerminalTokenEqual(url.searchParams.get('viewToken'), viewToken), () => readPlatformBinding() !== null, () => loadPersistedToken(DASHBOARD_TOKEN_PATH));
374
398
  }
375
399
  /** Lazily-written locked-mode zellij config for per-WS web-terminal attach
376
400
  * clients: cleared keybinds + locked mode so every keystroke passes straight
@@ -396,6 +420,16 @@ function cliName() { return CLI_DISPLAY_NAMES[lastInitConfig?.cliId ?? ''] ?? 'C
396
420
  let isPromptReady = false;
397
421
  /** Mutex for async flushPending — prevents concurrent flush loops. */
398
422
  let isFlushing = false;
423
+ /** True from the moment an owned CLI restart begins until the replacement
424
+ * backend has been synchronously installed. Async backends (notably Riff)
425
+ * keep the old backend object alive while destroySession() awaits remote
426
+ * cancellation; this gate prevents new input or an old idle callback from
427
+ * crossing that teardown fence. */
428
+ let cliRestartInProgress = false;
429
+ /** Raw slash commands require a real prompt and cannot use the ordinary
430
+ * type-ahead fallback. Keep them fenced across an owned restart until the
431
+ * replacement generation reaches its prompt. */
432
+ let rawInputRestartGate = false;
399
433
  /** Per-spawn one-shot: have this spawn's bot.startupCommands been typed in yet?
400
434
  * Reset in spawnCli so a restart/resume (which re-spawns the CLI) re-applies
401
435
  * them — needed because session-only settings like `/effort ultracode` are lost
@@ -634,8 +668,9 @@ async function runStartupCommands() {
634
668
  idleDetector?.reset();
635
669
  }
636
670
  const pendingMessages = [];
637
- /** Literal commands that arrived while native /rename owned the TUI. Normal
638
- * raw_input commands are still delivered immediately (including while busy). */
671
+ /** Literal commands that arrived while native /rename owned the TUI or while
672
+ * an owned CLI restart was fenced. Normal raw_input commands are still
673
+ * delivered immediately (including while busy). */
639
674
  const pendingRawInputs = [];
640
675
  /** Latest requested canonical session title. Unlike a normal prompt this is an
641
676
  * administrative TUI command: never type-ahead while the agent is busy, never
@@ -706,7 +741,9 @@ async function deliverRawInput(msg) {
706
741
  // Enter lands. sendToPty also observes commandLineWritesPending, so another
707
742
  // raw command's text -> Enter window cannot be interrupted by the follow-up.
708
743
  if (sent && msg.followUpContent) {
709
- sendToPty(msg.followUpContent, undefined, msg.followUpCodexAppInput);
744
+ sendToPty(msg.followUpContent, undefined, {
745
+ codexAppInput: msg.followUpCodexAppInput,
746
+ });
710
747
  log(`Enqueued follow-up after raw input (${msg.followUpContent.length} chars)`);
711
748
  }
712
749
  // A pending /rename may have been held by the command-write mutex. It still
@@ -720,29 +757,192 @@ const inflightInputs = new InflightInputTracker();
720
757
  * start work before their history/transcript submit marker is observable. */
721
758
  let lastPtyActivityAtMs = 0;
722
759
  let currentBotmuxTurnId;
723
- function readProcStarttime(pid) {
760
+ let currentBotmuxDispatchAttempt;
761
+ let currentVcMeetingImTurnOrigin;
762
+ let durableTurnInFlight = false;
763
+ function publishSandboxRelayCapability(opts = {}) {
764
+ const capability = {
765
+ token: randomBytes(32).toString('hex'),
766
+ ...(currentBotmuxTurnId ? { turnId: currentBotmuxTurnId } : {}),
767
+ ...(currentBotmuxDispatchAttempt !== undefined
768
+ ? { dispatchAttempt: currentBotmuxDispatchAttempt }
769
+ : {}),
770
+ };
771
+ const files = [
772
+ ...(sandboxRelayOutbox
773
+ ? [{
774
+ path: join(sandboxRelayOutbox, RELAY_ORIGIN_CAPABILITY_BASENAME),
775
+ body: JSON.stringify({ token: capability.token }),
776
+ }]
777
+ : []),
778
+ ...(readIsolationOriginCapabilityFile && sessionId
779
+ ? [{
780
+ path: readIsolationOriginCapabilityFile,
781
+ body: JSON.stringify({
782
+ sessionId,
783
+ capability: capability.token,
784
+ ...(capability.turnId ? { turnId: capability.turnId } : {}),
785
+ ...(capability.dispatchAttempt !== undefined
786
+ ? { dispatchAttempt: capability.dispatchAttempt }
787
+ : {}),
788
+ }),
789
+ }]
790
+ : []),
791
+ ];
792
+ for (const file of files) {
793
+ try {
794
+ replaceManagedOriginCapabilityFile(file.path, file.body);
795
+ }
796
+ catch (err) {
797
+ log(`Failed to publish managed origin capability: ${err?.message ?? err}`);
798
+ if (opts.failClosed) {
799
+ unlinkManagedOriginCapabilityFiles();
800
+ throw err;
801
+ }
802
+ }
803
+ }
804
+ sandboxRelayCapability = capability;
805
+ if (sessionId) {
806
+ send({
807
+ type: 'managed_turn_origin',
808
+ sessionId,
809
+ capability: capability.token,
810
+ ...(capability.turnId ? { turnId: capability.turnId } : {}),
811
+ ...(capability.dispatchAttempt !== undefined
812
+ ? { dispatchAttempt: capability.dispatchAttempt }
813
+ : {}),
814
+ });
815
+ }
816
+ }
817
+ function unlinkManagedOriginCapabilityFiles() {
818
+ const files = [
819
+ sandboxRelayOutbox
820
+ ? join(sandboxRelayOutbox, RELAY_ORIGIN_CAPABILITY_BASENAME)
821
+ : undefined,
822
+ readIsolationOriginCapabilityFile ?? undefined,
823
+ ];
824
+ for (const file of new Set(files.filter((p) => !!p))) {
825
+ try {
826
+ unlinkSync(file);
827
+ }
828
+ catch { /* absent or teardown racing */ }
829
+ }
830
+ }
831
+ /** Read a host-owned isolation marker without following a child-planted
832
+ * symlink between lookup and open. */
833
+ function readRegularHostFileNoFollow(filePath) {
834
+ let fd;
724
835
  try {
725
- const raw = readFileSync(`/proc/${pid}/stat`, 'utf8');
726
- const closeParen = raw.lastIndexOf(')');
727
- if (closeParen < 0)
728
- return undefined;
729
- const fields = raw.slice(closeParen + 2).trim().split(/\s+/);
730
- return fields[19] || undefined;
836
+ fd = openSync(filePath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
837
+ if (!fstatSync(fd).isFile())
838
+ return null;
839
+ return readFileSync(fd, 'utf8');
731
840
  }
732
841
  catch {
733
- return undefined;
842
+ return null;
843
+ }
844
+ finally {
845
+ if (fd !== undefined) {
846
+ try {
847
+ closeSync(fd);
848
+ }
849
+ catch { /* best-effort */ }
850
+ }
734
851
  }
735
852
  }
853
+ function completeManagedTurnOriginRevocation(revoked, turnId, dispatchAttempt) {
854
+ // Clear local authority before queuing daemon IPC. A forked/delayed process
855
+ // can otherwise win the small window between terminal publication and
856
+ // revocation by submitting through the still-live host relay.
857
+ sandboxRelayCapability = null;
858
+ currentVcMeetingImTurnOrigin = undefined;
859
+ if (sessionId) {
860
+ send({
861
+ type: 'managed_turn_origin_revoked',
862
+ sessionId,
863
+ ...(revoked ? { capability: revoked.token } : {}),
864
+ ...(turnId ? { turnId } : {}),
865
+ ...(dispatchAttempt !== undefined ? { dispatchAttempt } : {}),
866
+ });
867
+ }
868
+ unlinkManagedOriginCapabilityFiles();
869
+ }
870
+ /**
871
+ * Revoke this CLI generation's managed-send authority before an intentional
872
+ * in-worker restart starts tearing down its backend. The Node worker survives,
873
+ * so neither claude_exit nor worker-exit can perform the daemon-side cleanup.
874
+ *
875
+ * Keep currentBotmuxTurnId/currentBotmuxDispatchAttempt intact: the old
876
+ * backend's exit callback still needs that exact identity to emit its terminal
877
+ * edge. The relay token and explicit IM origin, however, become unusable
878
+ * synchronously; a later real turn publishes a fresh token in flushPending().
879
+ */
880
+ function revokeManagedTurnOriginForRestart() {
881
+ const revoked = sandboxRelayCapability;
882
+ completeManagedTurnOriginRevocation(revoked, currentBotmuxTurnId, currentBotmuxDispatchAttempt);
883
+ }
884
+ /** Revoke only the capability generation bound to this exact terminal. A late
885
+ * terminal from turn N must not clear the token already rotated for turn N+1. */
886
+ function revokeManagedTurnOriginForTerminal(turnId, dispatchAttempt) {
887
+ const revoked = sandboxRelayCapability;
888
+ if (!revoked
889
+ || revoked.turnId !== turnId
890
+ || revoked.dispatchAttempt !== dispatchAttempt)
891
+ return;
892
+ completeManagedTurnOriginRevocation(revoked, turnId, dispatchAttempt);
893
+ }
894
+ function authorizeManagedSend(claim) {
895
+ if (!sessionId)
896
+ return { ok: false, error: 'VC policy cannot resolve session id' };
897
+ const dataDir = process.env.SESSION_DATA_DIR;
898
+ if (!dataDir)
899
+ return { ok: false, error: 'VC policy cannot resolve session data' };
900
+ // The sandbox controls every byte in outbox/*.req.json. A rotating host-issued
901
+ // capability binds request creation to this exact live turn; the request may
902
+ // not name an older loud receipt, and a delayed request crossing a turn
903
+ // boundary is rejected after capability rotation.
904
+ const capability = sandboxRelayCapability;
905
+ if (!capability || !claim.capability || claim.capability !== capability.token) {
906
+ return { ok: false, error: 'origin_mismatch: relay capability is stale or missing' };
907
+ }
908
+ const session = sessionStore.getSession(sessionId);
909
+ const currentImOrigin = currentVcMeetingImTurnOrigin;
910
+ const imOrigin = currentImOrigin?.larkMessageId === capability.turnId
911
+ && currentImOrigin?.receiverSessionId === sessionId
912
+ ? currentImOrigin
913
+ : undefined;
914
+ const decision = evaluateVcMeetingManagedSend(dataDir, {
915
+ receiverSessionId: sessionId,
916
+ receiverSession: !!session?.vcMeetingReceiver,
917
+ turnId: capability.turnId,
918
+ dispatchAttempt: capability.dispatchAttempt,
919
+ currentImTurnOrigin: imOrigin,
920
+ });
921
+ return decision.ok
922
+ ? {
923
+ ok: true,
924
+ origin: {
925
+ ...(capability.turnId ? { turnId: capability.turnId } : {}),
926
+ ...(capability.dispatchAttempt !== undefined
927
+ ? { dispatchAttempt: capability.dispatchAttempt }
928
+ : {}),
929
+ },
930
+ }
931
+ : { ok: false, error: `${decision.errorCode}: ${decision.error}` };
932
+ }
736
933
  function writeCliPidMarker() {
737
934
  if (!cliPidMarker || !sessionId)
738
935
  return;
739
936
  try {
740
937
  // 原子写:daemon 侧(killStalePids 等)随时读这个 marker JSON。
741
938
  const markerPid = Number(basename(cliPidMarker));
742
- const procStart = Number.isInteger(markerPid) && markerPid > 0 ? readProcStarttime(markerPid) : undefined;
939
+ const procStart = Number.isInteger(markerPid) && markerPid > 0
940
+ ? readProcessStartIdentity(markerPid)
941
+ : undefined;
743
942
  atomicWriteFileSync(cliPidMarker, JSON.stringify({
744
943
  sessionId,
745
944
  turnId: currentBotmuxTurnId ?? null,
945
+ dispatchAttempt: currentBotmuxDispatchAttempt ?? null,
746
946
  ...(procStart ? { procStart } : {}),
747
947
  }));
748
948
  }
@@ -910,6 +1110,12 @@ let codexAdoptPendingPid;
910
1110
  * but BEFORE the rollout was located still reach the Lark thread. 5s
911
1111
  * skew tolerance is applied on top, mirroring the Lark/Claude bridges. */
912
1112
  let codexAdoptStartMs;
1113
+ /** Open-fd discovery is cheap via /proc on Linux but shells out to lsof on
1114
+ * macOS/BSD. Lark-driven Grok rotation is immediate through writeInput; this
1115
+ * throttled poll exists for direct terminal/adopt rotation and cold collision
1116
+ * recovery, where a few seconds of latency is acceptable. */
1117
+ let grokBridgePidProbeLastMs = 0;
1118
+ const GROK_BRIDGE_PID_PROBE_INTERVAL_MS = 5_000;
913
1119
  /** Adopt-only: 一次性发送的 "/adopt 前最后一轮" preamble 是否已经触发过。
914
1120
  * codexBridgeAttach 在 split-live 分支会查 history 取最后一对 user/assistant
915
1121
  * 发给 daemon —— late-attach poller 也会反复走这条分支(每秒一次),所以
@@ -1785,6 +1991,10 @@ function bridgeIngest() {
1785
1991
  if (result.events.length > 0)
1786
1992
  lastStructuredBridgeActivityAtMs = Date.now();
1787
1993
  bridgeQueue.ingest(result.events, bridgeJsonlPath);
1994
+ // Transcript terminal markers are authoritative and may settle a durable
1995
+ // turn immediately. Do not wait for the screen prompt: permission/AskUser
1996
+ // surfaces can resemble idle, while an explicit JSONL boundary cannot.
1997
+ emitReadyTurns({ explicitTerminalOnly: true });
1788
1998
  }
1789
1999
  function performBridgeIngestAndScheduleQuietEmit() {
1790
2000
  const beforePath = bridgeJsonlPath;
@@ -1922,6 +2132,10 @@ function stopBridgeWatcher() {
1922
2132
  bridgeStalePidStateSessionId = undefined;
1923
2133
  bridgeSecondaryPaths.clear();
1924
2134
  bridgeFingerprintScanLastMs.clear();
2135
+ // Attribution belongs to the CLI generation whose transcript we watched.
2136
+ // A fresh backend must not inherit a terminal/fingerprint from the exited
2137
+ // process; durable replay is owned by the receiver with a new attempt.
2138
+ bridgeQueue.clearPending();
1925
2139
  bridgePreambleSent = false;
1926
2140
  }
1927
2141
  /**
@@ -1940,12 +2154,13 @@ function stopBridgeWatcher() {
1940
2154
  * Lark turn.
1941
2155
  *
1942
2156
  * The turnId is returned so the writeInput failure path can call
1943
- * `bridgeQueue.dropPendingTurn(turnId)` after deferred recheck conclusively
1944
- * fails — otherwise an Enter-eaten-by-TUI submit leaves a fingerprint that
1945
- * no jsonl line will ever match, and `maybeSwitchBridgeJsonl` burns 99%
1946
- * CPU scanning all sibling jsonls for it on every poll tick.
2157
+ * `bridgeQueue.dropPendingTurn(turnId, dispatchAttempt)` after deferred
2158
+ * recheck conclusively fails — otherwise an Enter-eaten-by-TUI submit leaves
2159
+ * a fingerprint that no jsonl line will ever match, and
2160
+ * `maybeSwitchBridgeJsonl` burns 99% CPU scanning all sibling jsonls for it
2161
+ * on every poll tick.
1947
2162
  */
1948
- function bridgeMarkPendingTurn(messageText, preferredTurnId) {
2163
+ function bridgeMarkPendingTurn(messageText, preferredTurnId, dispatchAttempt) {
1949
2164
  if (!bridgeJsonlPath)
1950
2165
  return undefined;
1951
2166
  if (!bridgeBaselineDone) {
@@ -1982,7 +2197,7 @@ function bridgeMarkPendingTurn(messageText, preferredTurnId) {
1982
2197
  const normalised = normaliseForFingerprint(messageText);
1983
2198
  const contentNormalized = normalised.length > 0 ? normalised : undefined;
1984
2199
  const turnId = preferredTurnId ?? randomBytes(8).toString('hex');
1985
- bridgeQueue.mark(turnId, fingerprint, Date.now(), contentNormalized);
2200
+ bridgeQueue.mark(turnId, fingerprint, Date.now(), contentNormalized, dispatchAttempt);
1986
2201
  return turnId;
1987
2202
  }
1988
2203
  function bridgeDrainAndMaybeEmit() {
@@ -2003,8 +2218,13 @@ function bridgeDrainAndMaybeEmit() {
2003
2218
  *
2004
2219
  * Caches per-path drains so a batch of turns from the same file only reads
2005
2220
  * the transcript once (O(jsonl size) per distinct path). */
2006
- function emitReadyTurns() {
2007
- const ready = bridgeQueue.drainEmittable();
2221
+ function emitReadyTurns(opts = {}) {
2222
+ const ready = bridgeQueue.drainEmittable(opts.explicitTerminalOnly
2223
+ ? { explicitTerminalOnly: true }
2224
+ : {
2225
+ terminalBoundary: true,
2226
+ requireExplicitTerminalForDurable: true,
2227
+ });
2008
2228
  if (ready.length === 0)
2009
2229
  return;
2010
2230
  const adoptMode = lastInitConfig?.adoptMode === true;
@@ -2068,6 +2288,7 @@ function emitReadyTurns() {
2068
2288
  content: fields.content,
2069
2289
  lastUuid,
2070
2290
  turnId: turn.turnId,
2291
+ ...(turn.dispatchAttempt !== undefined ? { dispatchAttempt: turn.dispatchAttempt } : {}),
2071
2292
  kind: 'local-turn',
2072
2293
  userText: fields.userText,
2073
2294
  });
@@ -2082,12 +2303,25 @@ function emitReadyTurns() {
2082
2303
  content: headlessContent,
2083
2304
  lastUuid,
2084
2305
  turnId: turn.turnId,
2306
+ ...(turn.dispatchAttempt !== undefined ? { dispatchAttempt: turn.dispatchAttempt } : {}),
2085
2307
  kind: 'local-turn-headless',
2086
2308
  });
2087
2309
  continue;
2088
2310
  }
2089
- send({ type: 'final_output', content: assistantText, lastUuid, turnId: turn.turnId });
2311
+ send({
2312
+ type: 'final_output',
2313
+ content: assistantText,
2314
+ lastUuid,
2315
+ turnId: turn.turnId,
2316
+ ...(turn.dispatchAttempt !== undefined ? { dispatchAttempt: turn.dispatchAttempt } : {}),
2317
+ });
2090
2318
  }
2319
+ // A visible fallback is optional: it may be empty or deliberately suppressed
2320
+ // after the model used `botmux send`. Completion is not optional. Emit it
2321
+ // only after all corresponding final_output IPC messages have been queued so
2322
+ // the daemon observes a stable per-turn ordering.
2323
+ for (const turn of ready)
2324
+ emitTurnTerminal(turn.turnId, 'completed', undefined, turn.dispatchAttempt);
2091
2325
  }
2092
2326
  /** Drain `path` from `fromOffset` and feed the events to the bridge queue
2093
2327
  * with that path as the source stamp. Pure side-effects on bridgeQueue +
@@ -2115,10 +2349,11 @@ function codexBridgeFallbackActive() {
2115
2349
  // services/structured-bridge-clis.ts (single source of the allowlist).
2116
2350
  return isStructuredBridgeFallbackActive(lastInitConfig?.cliId, lastInitConfig?.adoptMode === true);
2117
2351
  }
2118
- // Both Codex and TRAE share the same rollout JSONL layout (response_item
2119
- // messages), so drainCodexRollout works for both.
2120
2352
  function structuredBridgeIsCodex() {
2121
- return lastInitConfig?.cliId === 'codex' || lastInitConfig?.cliId === 'traex';
2353
+ return lastInitConfig?.cliId === 'codex';
2354
+ }
2355
+ function structuredBridgeIsTraex() {
2356
+ return lastInitConfig?.cliId === 'traex';
2122
2357
  }
2123
2358
  function structuredBridgeIsHermes() {
2124
2359
  return lastInitConfig?.cliId === 'hermes';
@@ -2141,6 +2376,8 @@ function currentHermesBridgeDbPath() {
2141
2376
  function structuredBridgeIngestPath(path, offset) {
2142
2377
  if (structuredBridgeIsCodex())
2143
2378
  return drainCodexRollout(path, offset);
2379
+ if (structuredBridgeIsTraex())
2380
+ return drainTraexRollout(path, offset);
2144
2381
  if (codexBridgeIsCursor())
2145
2382
  return drainCursorTranscript(path, offset);
2146
2383
  if (structuredBridgeIsPi())
@@ -2222,6 +2459,17 @@ function codexBridgeStartTimer() {
2222
2459
  emitReadyCodexTurns();
2223
2460
  return;
2224
2461
  }
2462
+ // Grok keeps one process alive across `/new` / `/clear` / `/resume`, but
2463
+ // moves its authoritative updates stream to a new session directory.
2464
+ // A Lark-driven next prompt reports the new sid through writeInput;
2465
+ // adopt/local terminal input has no such callback, so follow the pid's
2466
+ // currently-open Grok session on every bridge tick as well.
2467
+ maybeFollowGrokSessionRotationViaPid();
2468
+ // TRAE has the same adopt/local-input blind spot: a direct `/new` in
2469
+ // the observed pane produces no adapter writeInput result. Its rollout
2470
+ // fd is process-scoped, so following that fd cannot select a sibling
2471
+ // TRAE process merely because it shares the working directory.
2472
+ maybeFollowTraexSessionRotationViaPid();
2225
2473
  if (!codexBridgeRolloutPath) {
2226
2474
  // Late-attach: cliSessionId (writeInput / daemon probe) then adopt
2227
2475
  // pid. Path lookup is centralized in resolveFileBridgePath so
@@ -2474,7 +2722,38 @@ function codexBridgeNotifyCliSessionId(cliSessionId) {
2474
2722
  if (!codexBridgeFallbackActive())
2475
2723
  return;
2476
2724
  if (codexBridgeRolloutPath) {
2477
- // Already attached — first-attach-wins for most CLIs. Exception: grok's
2725
+ // Already attached — first-attach-wins for most CLIs. Exceptions: TRAE
2726
+ // and Grok can rotate their native session in the same process.
2727
+ if (structuredBridgeIsTraex()) {
2728
+ const currentSid = codexSessionIdFromRolloutPath(codexBridgeRolloutPath);
2729
+ if (currentSid && currentSid.toLowerCase() === cliSessionId.toLowerCase())
2730
+ return;
2731
+ const next = resolveFileBridgePath('traex', { sessionId: cliSessionId });
2732
+ // Close any terminal already committed to the retired rollout before
2733
+ // switching. A durable turn is a worker HOL barrier, so a legitimate
2734
+ // session rotation cannot overtake an unfinished durable delivery.
2735
+ try {
2736
+ codexBridgeIngest();
2737
+ emitReadyCodexTurns();
2738
+ }
2739
+ catch (err) {
2740
+ log(`TRAE pre-rotation bridge drain failed: ${err.message}`);
2741
+ }
2742
+ if (next && next !== codexBridgeRolloutPath) {
2743
+ log(`TRAE session rotated ${currentSid ?? '?'} → ${cliSessionId}; re-attaching bridge to ${next}`);
2744
+ codexBridgeDetachFile();
2745
+ codexBridgePendingSessionId = undefined;
2746
+ codexBridgeAttach(next, 'fresh-empty');
2747
+ }
2748
+ else if (!next) {
2749
+ log(`TRAE session rotated ${currentSid ?? '?'} → ${cliSessionId}; waiting for rollout`);
2750
+ codexBridgeDetachFile();
2751
+ codexBridgePendingSessionId = cliSessionId;
2752
+ codexBridgeStartTimer();
2753
+ }
2754
+ return;
2755
+ }
2756
+ // Grok's
2478
2757
  // `/new` / `/clear` / `/resume` rotate to a NEW session directory at the
2479
2758
  // SAME pid, and writeInput's prompt_history verify reports the new
2480
2759
  // session id on the very next submit. Without a re-attach the bridge
@@ -2490,6 +2769,17 @@ function codexBridgeNotifyCliSessionId(cliSessionId) {
2490
2769
  });
2491
2770
  if (next && next !== codexBridgeRolloutPath) {
2492
2771
  log(`Grok session rotated ${currentSid ?? '?'} → ${cliSessionId}; re-attaching bridge to ${next}`);
2772
+ // Drain the retired stream synchronously before closing its watcher.
2773
+ // A turn_completed may already be on disk while fs.watch / the 1s
2774
+ // poller is still queued; letting the new path replace it first would
2775
+ // strand the exact old turn until the durable lease expires.
2776
+ try {
2777
+ codexBridgeIngest();
2778
+ emitReadyCodexTurns();
2779
+ }
2780
+ catch (err) {
2781
+ log(`Grok pre-rotation bridge drain failed: ${err.message}`);
2782
+ }
2493
2783
  codexBridgeDetachFile();
2494
2784
  codexBridgePendingSessionId = undefined;
2495
2785
  codexBridgeAttach(next, 'fresh-empty');
@@ -2498,6 +2788,13 @@ function codexBridgeNotifyCliSessionId(cliSessionId) {
2498
2788
  // New session id reported but updates.jsonl not on disk yet —
2499
2789
  // detach the retired session and arm the poller for late-attach.
2500
2790
  log(`Grok session rotated ${currentSid ?? '?'} → ${cliSessionId}; waiting for updates.jsonl`);
2791
+ try {
2792
+ codexBridgeIngest();
2793
+ emitReadyCodexTurns();
2794
+ }
2795
+ catch (err) {
2796
+ log(`Grok pre-rotation bridge drain failed: ${err.message}`);
2797
+ }
2501
2798
  codexBridgeDetachFile();
2502
2799
  codexBridgePendingSessionId = cliSessionId;
2503
2800
  codexBridgeStartTimer();
@@ -2544,7 +2841,52 @@ function codexBridgeNotifyCliSessionId(cliSessionId) {
2544
2841
  codexBridgeStartTimer();
2545
2842
  }
2546
2843
  }
2547
- function codexBridgeIngest() {
2844
+ /** Follow a Grok in-process session rotation even when no botmux writeInput
2845
+ * occurred (notably direct terminal input in adopt mode). Open-fd discovery is
2846
+ * process-scoped, so sibling Grok sessions in the same cwd cannot steal this
2847
+ * bridge. `codexBridgeNotifyCliSessionId` owns the drain-before-detach switch. */
2848
+ function maybeFollowGrokSessionRotationViaPid() {
2849
+ if (!structuredBridgeIsGrok() || !codexBridgeRolloutPath || !backend)
2850
+ return;
2851
+ const now = Date.now();
2852
+ if (now - grokBridgePidProbeLastMs < GROK_BRIDGE_PID_PROBE_INTERVAL_MS)
2853
+ return;
2854
+ grokBridgePidProbeLastMs = now;
2855
+ const pid = backend.cliPid
2856
+ ?? backend.getChildPid?.()
2857
+ ?? codexAdoptPendingPid;
2858
+ if (!pid)
2859
+ return;
2860
+ const observed = findGrokSessionByPid(pid);
2861
+ if (!observed)
2862
+ return;
2863
+ const currentSid = grokSessionIdFromPath(codexBridgeRolloutPath);
2864
+ if (currentSid?.toLowerCase() === observed.sessionId.toLowerCase())
2865
+ return;
2866
+ persistCliSessionId(observed.sessionId);
2867
+ codexBridgeNotifyCliSessionId(observed.sessionId);
2868
+ }
2869
+ /** Follow a TRAE in-process session rotation for direct local input in an
2870
+ * adopted pane. `codexBridgeNotifyCliSessionId` performs the drain-before-
2871
+ * detach switch and persists the newly observed native session id. */
2872
+ function maybeFollowTraexSessionRotationViaPid() {
2873
+ if (!structuredBridgeIsTraex() || !codexBridgeRolloutPath || !backend)
2874
+ return;
2875
+ const pid = backend.cliPid
2876
+ ?? backend.getChildPid?.()
2877
+ ?? codexAdoptPendingPid;
2878
+ if (!pid)
2879
+ return;
2880
+ const observed = findTraexRolloutByPid(pid);
2881
+ if (!observed)
2882
+ return;
2883
+ const currentSid = codexSessionIdFromRolloutPath(codexBridgeRolloutPath);
2884
+ if (currentSid?.toLowerCase() === observed.cliSessionId.toLowerCase())
2885
+ return;
2886
+ persistCliSessionId(observed.cliSessionId);
2887
+ codexBridgeNotifyCliSessionId(observed.cliSessionId);
2888
+ }
2889
+ function codexBridgeIngest(opts = {}) {
2548
2890
  if (structuredBridgeIsHermes()) {
2549
2891
  hermesBridgeIngest();
2550
2892
  return;
@@ -2567,26 +2909,26 @@ function codexBridgeIngest() {
2567
2909
  // its own moving targets). Pushing idle here lets the bridge emit
2568
2910
  // immediately instead of waiting for readyPattern + quiescence to
2569
2911
  // converge. Idempotent — IdleDetector.fireIdle no-ops while already idle.
2570
- if (result.events.some(e => e.kind === 'assistant_final')) {
2912
+ if (opts.signalIdle !== false && result.events.some(e => e.kind === 'assistant_final')) {
2571
2913
  idleDetector?.fireIdle();
2572
2914
  }
2573
2915
  }
2574
2916
  /** Mark a pending Lark turn for Codex. Crucially this works even before a
2575
2917
  * rollout path is known — the queue is path-agnostic, and ingest after
2576
2918
  * late-attach picks up the user_message and matches the fingerprint. */
2577
- function codexBridgeMarkPendingTurn(messageText, preferredTurnId) {
2919
+ function codexBridgeMarkPendingTurn(messageText, preferredTurnId, dispatchAttempt) {
2578
2920
  if (!codexBridgeFallbackActive())
2579
2921
  return false;
2580
2922
  const turnId = preferredTurnId ?? `codex-${randomBytes(8).toString('hex')}`;
2581
- codexBridgeQueue.mark(turnId, messageText);
2923
+ codexBridgeQueue.mark(turnId, messageText, Date.now(), dispatchAttempt);
2582
2924
  return true;
2583
2925
  }
2584
- function codexBridgeDrainAndMaybeEmit() {
2926
+ function codexBridgeDrainAndMaybeEmit(opts = {}) {
2585
2927
  if (!codexBridgeFallbackActive())
2586
2928
  return;
2587
2929
  if (structuredBridgeIsHermes() || structuredBridgeIsMtr() || (codexBridgeRolloutPath && codexBridgeBaselineDone)) {
2588
2930
  try {
2589
- codexBridgeIngest();
2931
+ codexBridgeIngest(opts);
2590
2932
  }
2591
2933
  catch (err) {
2592
2934
  log(`Codex bridge ingest error: ${err.message}`);
@@ -2639,6 +2981,7 @@ function emitReadyCodexTurns() {
2639
2981
  content: fields.content,
2640
2982
  lastUuid: turn.turnId,
2641
2983
  turnId: turn.turnId,
2984
+ ...(turn.dispatchAttempt !== undefined ? { dispatchAttempt: turn.dispatchAttempt } : {}),
2642
2985
  kind: 'local-turn',
2643
2986
  userText: fields.userText,
2644
2987
  });
@@ -2650,8 +2993,12 @@ function emitReadyCodexTurns() {
2650
2993
  content: turn.finalText,
2651
2994
  lastUuid: turn.turnId,
2652
2995
  turnId: turn.turnId,
2996
+ ...(turn.dispatchAttempt !== undefined ? { dispatchAttempt: turn.dispatchAttempt } : {}),
2653
2997
  });
2654
2998
  }
2999
+ for (const turn of ready) {
3000
+ emitTurnTerminal(turn.turnId, turn.terminalStatus ?? 'completed', turn.terminalErrorCode, turn.dispatchAttempt);
3001
+ }
2655
3002
  }
2656
3003
  function stopCodexBridge() {
2657
3004
  if (codexBridgeWatcher) {
@@ -2680,6 +3027,7 @@ function stopCodexBridge() {
2680
3027
  codexBridgePendingSessionId = undefined;
2681
3028
  codexAdoptPendingPid = undefined;
2682
3029
  codexAdoptStartMs = undefined;
3030
+ grokBridgePidProbeLastMs = 0;
2683
3031
  }
2684
3032
  /** When a rotation moves bridgeJsonlPath away from `oldPath`, queue turns
2685
3033
  * whose sourceJsonlPath equals oldPath may still be waiting on assistant
@@ -2772,6 +3120,9 @@ const SCREEN_UPDATE_INTERVAL_MS = 2_000;
2772
3120
  // ─── Scrollback Buffer (replay to late-connecting WS clients) ───────────────
2773
3121
  const MAX_SCROLLBACK = 1_000_000; // chars (~1MB)
2774
3122
  let scrollback = '';
3123
+ let herdrWebHistory = null;
3124
+ let herdrWebScrollDirection = null;
3125
+ let herdrWebCursor = null;
2775
3126
  const WORKFLOW_TRANSCRIPT_MAX = 2_000_000; // chars (~2MB)
2776
3127
  const WORKFLOW_OUTPUT_END_MARKER = '</WORKFLOW_OUTPUT>';
2777
3128
  const CRASH_DIAGNOSTIC_RAW_MAX = 200_000; // enough scrollback for the web terminal without huge temp files
@@ -2787,6 +3138,68 @@ let workflowFinalOutputSent = false;
2787
3138
  let altBufferActive = false;
2788
3139
  const ALT_ENTER_RE = /\x1b\[\?(1049|1047|47)h/g;
2789
3140
  const ALT_EXIT_RE = /\x1b\[\?(1049|1047|47)l/g;
3141
+ function usesHerdrSnapshotWebHistory() {
3142
+ return backend instanceof HerdrBackend;
3143
+ }
3144
+ function herdrWebCursorSequence(cursor = herdrWebCursor) {
3145
+ return cursor ? `\x1b[${cursor.row + 1};${cursor.col + 1}H` : '';
3146
+ }
3147
+ function relayHerdrWebCursor(cursor) {
3148
+ herdrWebCursor = cursor;
3149
+ const sequence = herdrWebCursorSequence(cursor);
3150
+ for (const ws of wsClients) {
3151
+ if (ws.readyState === WebSocket.OPEN)
3152
+ ws.send(sequence);
3153
+ }
3154
+ }
3155
+ function relayHerdrWebSnapshot(snapshot) {
3156
+ const merged = mergeHerdrWebSnapshot(herdrWebHistory, snapshot, herdrWebScrollDirection, MAX_SCROLLBACK);
3157
+ herdrWebHistory = merged.state;
3158
+ herdrWebScrollDirection = null;
3159
+ scrollback = renderHerdrWebHistory(merged.state);
3160
+ const payload = `\x1b]1989;history;${merged.addedLines}\x07${scrollback}${herdrWebCursorSequence()}`;
3161
+ for (const ws of wsClients) {
3162
+ if (ws.readyState === WebSocket.OPEN)
3163
+ ws.send(payload);
3164
+ }
3165
+ }
3166
+ function applyHerdrWebBindingResult(ws, result) {
3167
+ if (ws.readyState !== WebSocket.OPEN)
3168
+ return;
3169
+ const { backend: herdrWebBackend, initialSize, size } = result;
3170
+ if (initialSize) {
3171
+ ws.send(`\x1b]1989;follower;${initialSize.cols};${initialSize.rows}\x07`);
3172
+ }
3173
+ if (!herdrWebBackend || !size)
3174
+ return;
3175
+ for (const client of wsClients) {
3176
+ if (client.readyState === WebSocket.OPEN &&
3177
+ !herdrWebBackend.isWebTerminalOwner(client)) {
3178
+ client.send(`\x1b]1989;follower;${size.cols};${size.rows}\x07`);
3179
+ }
3180
+ }
3181
+ }
3182
+ /**
3183
+ * /restart replaces a managed Herdr backend without closing browser sockets.
3184
+ * Restore every viewer in connection order so the previous oldest surviving
3185
+ * owner remains authoritative, then re-apply its last browser grid.
3186
+ */
3187
+ function restoreHerdrWebBindings() {
3188
+ if (!(backend instanceof HerdrBackend) || lastInitConfig?.adoptMode)
3189
+ return;
3190
+ for (const [ws, binding] of herdrWebBindings) {
3191
+ if (ws.readyState !== WebSocket.OPEN) {
3192
+ binding.release();
3193
+ herdrWebBindings.delete(ws);
3194
+ continue;
3195
+ }
3196
+ applyHerdrWebBindingResult(ws, binding.restore());
3197
+ }
3198
+ }
3199
+ function wireHerdrWebTerminalRelays(be) {
3200
+ be.onSnapshot(relayHerdrWebSnapshot);
3201
+ be.onWebTerminalCursor(relayHerdrWebCursor);
3202
+ }
2790
3203
  function recentTerminalLogTail() {
2791
3204
  const plain = stripAnsiForLog(tailChars(scrollback, CRASH_DIAGNOSTIC_RAW_MAX));
2792
3205
  if (!plain)
@@ -2968,7 +3381,7 @@ function startScreenAnalyzer() {
2968
3381
  onAnalyzing: () => { },
2969
3382
  onTuiPrompt: (description, options, multiSelect) => {
2970
3383
  tuiPromptBlocking = true;
2971
- send({ type: 'tui_prompt', description, options, multiSelect, turnId: currentBotmuxTurnId });
3384
+ send({ type: 'tui_prompt', description, options, multiSelect, turnId: currentBotmuxTurnId, dispatchAttempt: currentBotmuxDispatchAttempt });
2972
3385
  },
2973
3386
  onTuiPromptResolved: (selectedText) => {
2974
3387
  tuiPromptBlocking = false;
@@ -3254,6 +3667,78 @@ async function handleTuiKeys(keys, isFinal) {
3254
3667
  }
3255
3668
  log(`TUI keys: ${keys.join(' ')}${isFinal ? ' (final)' : ''}`);
3256
3669
  }
3670
+ // 待注入的 TUI 命令队列。生命周期绑定当前 CLI 进程:killCli() 会清空它,
3671
+ // 防止 restart 后残留命令重放进新 CLI——新增清理状态时记得同步那里。
3672
+ // barrier=true 标记该注入携带 updateWorkingDir(cwd-move,如 /cd):markPromptReady
3673
+ // 里 barrier 注入必须先于本次 pending 用户消息落地,防止用户消息在「记录已是新
3674
+ // cwd、进程仍在旧 cwd」的窗口里被写进旧目录的 CLI。排队策略判定收敛到
3675
+ // core/inject-queue-policy.ts(shouldDeferUserFlush / shouldFlushInjectionsFirst)
3676
+ // 以获得可单测的纯函数单元——本文件只持有队列状态。
3677
+ const pendingInjections = [];
3678
+ let injectionFlushing = false;
3679
+ /** 排队注入一行 TUI 命令:idle(isPromptReady)时经 sendRawCommandLineSerially 敲入。 */
3680
+ async function flushPendingInjections() {
3681
+ // 不跨 restart 边界写入(与 flushPending 同款守卫):destroySession 异步期间
3682
+ // backend 可能仍指向旧 CLI。
3683
+ if (cliRestartInProgress)
3684
+ return;
3685
+ // 与其它 PTY 写入方的互斥判定收敛在 canStartInjectionFlush(纯函数,可单测):
3686
+ // - 用户消息 flush 持锁中(isFlushing):markPromptReady 没有 isFlushing 守卫,
3687
+ // 可能在 flush 中途被误 idle 触发(startup-command 的 quiescence 等待、慢
3688
+ // submit-verify 的 text→Enter 间隙都是多秒级窗口)——此时开始注入会把命令
3689
+ // 拼进已敲了半条用户消息的 composer。flushPending 反向检查 injectionFlushing,
3690
+ // 互斥必须对称才成立。
3691
+ // - /rename 原生同步在飞(sessionRenameInFlight)与字面命令行写入窗口
3692
+ // (commandLineWritesPending):raw-write 围栏,注入同为 raw 命令行写入方。
3693
+ // 被挡下的注入不丢:队列留存,竞争写入方结束后的下一次 markPromptReady 再踢。
3694
+ if (!canStartInjectionFlush({
3695
+ injectionFlushing,
3696
+ userFlushing: isFlushing,
3697
+ sessionRenameInFlight,
3698
+ commandLineWritesPending,
3699
+ bareShellLaunchBlocked,
3700
+ }))
3701
+ return;
3702
+ injectionFlushing = true;
3703
+ try {
3704
+ while (pendingInjections.length > 0 && backend && isPromptReady && !bareShellLaunchBlocked
3705
+ && !sessionRenameInFlight) {
3706
+ // Mirror flushPending's one-shot launch-failure guard: when an injection is
3707
+ // the FIRST writer after a (re)spawn, flushPending may never have run
3708
+ // (pendingMessages empty ⇒ early return) so the bare-shell check must also
3709
+ // fire here before typing. Shares the same one-shot flag — whichever flush
3710
+ // path sends first runs the detection.
3711
+ if (!bareShellChecked) {
3712
+ bareShellChecked = true;
3713
+ if (detectBareShellLaunch())
3714
+ return; // finally{} releases the mutex; queue stays
3715
+ }
3716
+ const item = pendingInjections.shift();
3717
+ const cmd = item.command;
3718
+ isPromptReady = false;
3719
+ idleDetector?.reset();
3720
+ try {
3721
+ // Serially:与 startupCommands / raw_input / native-rename 共用同一条
3722
+ // 字面命令行写入互斥链(text → beat → Enter 窗口不被拼接)。
3723
+ await sendRawCommandLineSerially(backend, cmd);
3724
+ await awaitPtyQuiescence(STARTUP_CMD_QUIET_MS, STARTUP_CMD_CAP_MS);
3725
+ log(`Injected command: ${cmd}`);
3726
+ }
3727
+ catch (e) {
3728
+ log(`Inject command failed (${cmd}): ${e?.message ?? e}`);
3729
+ }
3730
+ }
3731
+ }
3732
+ finally {
3733
+ injectionFlushing = false;
3734
+ // 若注入期间有用户输入(消息 / raw input / rename)被 flushPending 的
3735
+ // injectionFlushing 守卫挡下、且此刻已重新 idle,补踢一次(flushPending
3736
+ // 自带全部守卫,最坏 no-op)。
3737
+ if (isPromptReady && (pendingMessages.length > 0 || pendingRawInputs.length > 0 || pendingSessionRename !== null)) {
3738
+ void flushPending();
3739
+ }
3740
+ }
3741
+ }
3257
3742
  /**
3258
3743
  * Handle atomic text-input: navigate to "Type something" (WITHOUT pressing Enter),
3259
3744
  * then write text via cliAdapter (which adds its own Enter to submit).
@@ -3422,9 +3907,8 @@ function dismissAidenCodexUpdateDialog(data) {
3422
3907
  // Codex App runner sends botmux control messages as OSC sequences so they do
3423
3908
  // not pollute the visible terminal. Strip them before xterm rendering and
3424
3909
  // translate them back into worker IPC.
3425
- const CODEX_APP_OSC_PREFIX = '\x1b]777;botmux:';
3426
3910
  const APP_RUNNER_OSC_CLI_IDS = new Set(['codex-app', 'mira', 'mir']);
3427
- let codexAppOscPending = '';
3911
+ const appRunnerControlDecoder = new RunnerControlDecoder();
3428
3912
  let kiroSessionIdCaptureArmed = false;
3429
3913
  let kiroSessionIdCaptureBuffer = '';
3430
3914
  function decodeCodexAppPayload(payload) {
@@ -3450,20 +3934,45 @@ function handleCodexAppMarker(body) {
3450
3934
  if (kind === 'final' && typeof payload.content === 'string') {
3451
3935
  const startedAtMs = typeof payload.startedAtMs === 'number' ? payload.startedAtMs : undefined;
3452
3936
  const completedAtMs = typeof payload.completedAtMs === 'number' ? payload.completedAtMs : Date.now();
3937
+ // Codex App keeps the app-server-generated id separately for diagnostics.
3938
+ // Routing must use its stable clientUserMessageId marker; legacy envelopes
3939
+ // intentionally omit it and fall back to the worker's frozen botmux turn.
3940
+ const identity = resolveCodexAppFinalTurnIdentity(payload, currentBotmuxTurnId, `${lastInitConfig?.cliId ?? 'app'}-${Date.now()}`);
3941
+ if (!identity.ok) {
3942
+ log(`${cliName()} rejected final marker with mismatched turn `
3943
+ + `(marker=${identity.markerTurnId.substring(0, 12)}, `
3944
+ + `current=${identity.currentBotmuxTurnId?.substring(0, 12) ?? '-'})`);
3945
+ return;
3946
+ }
3947
+ const { turnId, nativeTurnId } = identity;
3948
+ if (nativeTurnId && nativeTurnId !== turnId) {
3949
+ log(`${cliName()} native turn ${nativeTurnId.substring(0, 12)} mapped to botmux turn ${turnId.substring(0, 12)}`);
3950
+ }
3951
+ if (payload.dispatchAttempt !== undefined
3952
+ && payload.dispatchAttempt !== currentBotmuxDispatchAttempt) {
3953
+ log(`${cliName()} rejected final marker with mismatched dispatch attempt `
3954
+ + `(marker=${String(payload.dispatchAttempt)}, current=${currentBotmuxDispatchAttempt ?? '-'})`);
3955
+ return;
3956
+ }
3957
+ // Attempt authority is worker-owned. A runner marker may redundantly assert
3958
+ // equality for compatibility, but can never select another attempt.
3959
+ const dispatchAttempt = currentBotmuxDispatchAttempt;
3453
3960
  if (startedAtMs !== undefined) {
3454
3961
  const sentByModel = shouldSuppressBridgeEmit({ markTimeMs: startedAtMs, isLocal: false, finalText: payload.content }, completedAtMs + 5_001, readSendMarkers(), false);
3455
3962
  if (sentByModel) {
3456
3963
  log(`${cliName()} final_output suppressed (model already called botmux send)`);
3964
+ emitTurnTerminal(turnId, 'completed', undefined, dispatchAttempt);
3457
3965
  return;
3458
3966
  }
3459
3967
  }
3460
- const turnId = typeof payload.turnId === 'string' ? payload.turnId : (currentBotmuxTurnId ?? `${lastInitConfig?.cliId ?? 'app'}-${Date.now()}`);
3461
3968
  send({
3462
3969
  type: 'final_output',
3463
3970
  content: payload.content,
3464
3971
  lastUuid: turnId,
3465
3972
  turnId,
3973
+ ...(dispatchAttempt !== undefined ? { dispatchAttempt } : {}),
3466
3974
  });
3975
+ emitTurnTerminal(turnId, 'completed', undefined, dispatchAttempt);
3467
3976
  }
3468
3977
  }
3469
3978
  function maybeCaptureKiroSessionId(data) {
@@ -3478,36 +3987,7 @@ function maybeCaptureKiroSessionId(data) {
3478
3987
  kiroSessionIdCaptureBuffer = '';
3479
3988
  }
3480
3989
  function splitCodexAppControl(data) {
3481
- if (!APP_RUNNER_OSC_CLI_IDS.has(lastInitConfig?.cliId ?? '') && codexAppOscPending.length === 0)
3482
- return data;
3483
- const input = codexAppOscPending + data;
3484
- codexAppOscPending = '';
3485
- let out = '';
3486
- let cursor = 0;
3487
- for (;;) {
3488
- const start = input.indexOf(CODEX_APP_OSC_PREFIX, cursor);
3489
- if (start < 0) {
3490
- let tailStart = input.length;
3491
- const tail = input.slice(cursor);
3492
- for (let n = Math.min(CODEX_APP_OSC_PREFIX.length - 1, tail.length); n > 0; n--) {
3493
- if (CODEX_APP_OSC_PREFIX.startsWith(tail.slice(tail.length - n))) {
3494
- tailStart = input.length - n;
3495
- break;
3496
- }
3497
- }
3498
- out += input.slice(cursor, tailStart);
3499
- codexAppOscPending = input.slice(tailStart);
3500
- return out;
3501
- }
3502
- out += input.slice(cursor, start);
3503
- const end = input.indexOf('\x07', start + CODEX_APP_OSC_PREFIX.length);
3504
- if (end < 0) {
3505
- codexAppOscPending = input.slice(start);
3506
- return out;
3507
- }
3508
- handleCodexAppMarker(input.slice(start + CODEX_APP_OSC_PREFIX.length, end));
3509
- cursor = end + 1;
3510
- }
3990
+ return appRunnerControlDecoder.push(data, APP_RUNNER_OSC_CLI_IDS.has(lastInitConfig?.cliId ?? ''), handleCodexAppMarker);
3511
3991
  }
3512
3992
  // ─── Prompt Detection ────────────────────────────────────────────────────────
3513
3993
  function onPtyData(data) {
@@ -3522,7 +4002,7 @@ function onPtyData(data) {
3522
4002
  // no relay needed. In non-tmux mode AND in pipe mode (adopt-bridge),
3523
4003
  // broadcast through the shared scrollback so all connected web clients
3524
4004
  // render the same byte stream.
3525
- if (!isTmuxMode || isPipeMode) {
4005
+ if ((!isTmuxMode || isPipeMode) && !usesHerdrSnapshotWebHistory()) {
3526
4006
  // Track alt-buffer state so we can restore it in the scrollback prefix.
3527
4007
  // Scan for the *last* toggle in this chunk — that's the current state.
3528
4008
  let lastToggleIdx = -1;
@@ -3583,6 +4063,12 @@ function onPtyData(data) {
3583
4063
  lastPtyOutputAtMs = Date.now();
3584
4064
  idleDetector?.feed(data);
3585
4065
  }
4066
+ function releaseRawInputRestartGate() {
4067
+ if (!rawInputRestartGate)
4068
+ return;
4069
+ rawInputRestartGate = false;
4070
+ log('Replacement CLI prompt ready — releasing deferred passthrough commands');
4071
+ }
3586
4072
  function markPromptReady() {
3587
4073
  if (isPromptReady)
3588
4074
  return; // guard against duplicate calls
@@ -3604,6 +4090,11 @@ function markPromptReady() {
3604
4090
  }
3605
4091
  isPromptReady = true;
3606
4092
  clearSessionRenameInFlight();
4093
+ // An old backend can still report idle while its async teardown is running.
4094
+ // Only a prompt observed after the general restart fence drops may release
4095
+ // slash commands to the replacement generation.
4096
+ if (!cliRestartInProgress)
4097
+ releaseRawInputRestartGate();
3607
4098
  // CLI 实际启动成功(回到 prompt):复位连续重启计数。
3608
4099
  // 任何能到这一步的 spawn 都算"成功"——后续即便再崩溃(不是 resume 目标不存在
3609
4100
  // 的问题),下一轮也该有新的 2 次重试预算,而不是被历史重启计数卡住。
@@ -3614,7 +4105,12 @@ function markPromptReady() {
3614
4105
  // CLI is back at its prompt — every previously written input has been
3615
4106
  // consumed, so nothing is in flight anymore. A later crash must not
3616
4107
  // replay these.
3617
- inflightInputs.onTurnComplete();
4108
+ // Screen-idle is sufficient for ordinary IM batching, but durable input is
4109
+ // cleared only by its exact turn_terminal. If this is a false idle and the
4110
+ // CLI exits, onCliExit still reports ambiguous while deliberately declining
4111
+ // worker-local replay of the durable attempt.
4112
+ if (!durableTurnInFlight)
4113
+ inflightInputs.onTurnComplete();
3618
4114
  maybeEmitWorkflowTranscriptOutput();
3619
4115
  if (awaitingFirstPrompt) {
3620
4116
  awaitingFirstPrompt = false;
@@ -3628,9 +4124,23 @@ function markPromptReady() {
3628
4124
  // (where the initial prompt is queued before the CLI becomes idle).
3629
4125
  if (renderer && pendingMessages.length === 0 && pendingRawInputs.length === 0 && pendingSessionRename === null && !isFlushing) {
3630
4126
  const { content } = renderer.snapshot();
3631
- send({ type: 'screen_update', content, ...usageLimitTracker.classify(content, 'idle'), turnId: currentBotmuxTurnId });
4127
+ send({ type: 'screen_update', content, ...usageLimitTracker.classify(content, 'idle'), turnId: currentBotmuxTurnId, dispatchAttempt: currentBotmuxDispatchAttempt });
4128
+ }
4129
+ // cwd-move 注入(barrier=true,如 /cd)必须先于本次 pending 用户消息落地:
4130
+ // 用户在 bot 发完「已切换角色」确认后立刻发下一条消息是最常见路径——若
4131
+ // flushPending 先跑,该消息会在 barrier 注入之前被写进旧 cwd 的 CLI(daemon
4132
+ // 记录已是新目录,实际执行仍在旧目录)。跳过本次 flushPending 不会饿死用户
4133
+ // 消息:flushPending 自身的 injectionFlushing 守卫会挡住并发写入,
4134
+ // flushPendingInjections 的 finally 会在注入完成、CLI 重新 idle 后补踢一次
4135
+ // flushPending 排空 pendingMessages。
4136
+ if (shouldFlushInjectionsFirst(pendingInjections)) {
4137
+ void flushPendingInjections();
4138
+ }
4139
+ else {
4140
+ flushPending();
4141
+ if (pendingInjections.length > 0)
4142
+ void flushPendingInjections();
3632
4143
  }
3633
- flushPending();
3634
4144
  }
3635
4145
  function persistCliSessionId(cliSessionId) {
3636
4146
  if (!cliSessionId || !sessionId)
@@ -3709,20 +4219,25 @@ const SUBMIT_DEFERRED_RECHECK_MS = 20_000;
3709
4219
  *
3710
4220
  * `bridgeTurnId` is the BridgeTurnQueue mark created right before the
3711
4221
  * failing writeInput. When the deferred recheck conclusively fails (= no
3712
- * jsonl line will ever match this fingerprint), we drop the mark — leaving
3713
- * it would keep `maybeSwitchBridgeJsonl` doing full-directory scans every
3714
- * poll tick for a fingerprint that's permanently dead, the 99% CPU bug
3715
- * this whole patch series is fixing. */
3716
- function scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId, failureReason, turnSeq = usageLimitTracker.currentTurn()) {
4222
+ * jsonl line will ever match this fingerprint), we drop that exact dispatch
4223
+ * attempt's mark — leaving it would keep `maybeSwitchBridgeJsonl` doing
4224
+ * full-directory scans every poll tick for a fingerprint that's permanently
4225
+ * dead, the 99% CPU bug this whole patch series is fixing. */
4226
+ function scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId, failureReason, turnSeq = usageLimitTracker.currentTurn(), turnIdentity) {
3717
4227
  const preview = msg.length > 60 ? msg.slice(0, 60) + '…' : msg;
4228
+ const emitDurableFailure = (errorCode) => {
4229
+ if (turnIdentity?.turnId && turnIdentity.dispatchAttempt !== undefined) {
4230
+ emitTurnTerminal(turnIdentity.turnId, 'failed', errorCode, turnIdentity.dispatchAttempt);
4231
+ }
4232
+ };
3718
4233
  const dropBridgeMark = () => {
3719
4234
  if (!bridgeTurnId)
3720
4235
  return;
3721
- const dropped = bridgeQueue.dropPendingTurn(bridgeTurnId);
4236
+ const dropped = bridgeQueue.dropPendingTurn(bridgeTurnId, turnIdentity?.dispatchAttempt);
3722
4237
  if (dropped) {
3723
4238
  if (dropped.contentFingerprint)
3724
4239
  bridgeFingerprintScanLastMs.delete(dropped.contentFingerprint);
3725
- log(`Bridge mark dropped after submit failure (turnId=${bridgeTurnId}) — rotation-fallback scan will stop spinning on this fingerprint.`);
4240
+ log(`Bridge mark dropped after submit failure (turnId=${bridgeTurnId}, attempt=${turnIdentity?.dispatchAttempt ?? '-'}) — rotation-fallback scan will stop spinning on this fingerprint.`);
3726
4241
  }
3727
4242
  };
3728
4243
  if (failureReason) {
@@ -3734,11 +4249,14 @@ function scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId
3734
4249
  dropBridgeMark();
3735
4250
  const reason = action.kind === 'notify-hard-failure' ? action.reason : failureReason;
3736
4251
  log(`writeInput: submit impossible — notifying user immediately. reason="${reason}" preview="${preview}"`);
3737
- send({
3738
- type: 'user_notify',
3739
- turnId: currentBotmuxTurnId,
3740
- message: t('worker.submit_impossible', { cliName: cliName(), reason, preview }),
3741
- });
4252
+ emitDurableFailure(`submit_impossible:${reason}`);
4253
+ if (turnIdentity?.dispatchAttempt === undefined) {
4254
+ send({
4255
+ type: 'user_notify',
4256
+ turnId: turnIdentity?.turnId ?? currentBotmuxTurnId,
4257
+ message: t('worker.submit_impossible', { cliName: cliName(), reason, preview }),
4258
+ });
4259
+ }
3742
4260
  return;
3743
4261
  }
3744
4262
  const activityBaselineMs = Date.now();
@@ -3777,9 +4295,16 @@ function scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId
3777
4295
  case 'suppress-usage-limit':
3778
4296
  dropBridgeMark();
3779
4297
  log(`Deferred recheck missing but usage limit was detected for this turn — suppressing submit warning. preview="${preview}"`);
4298
+ emitDurableFailure('submit_usage_limit');
3780
4299
  return;
3781
4300
  case 'suppress-active':
3782
4301
  log(`Deferred recheck missing but later ${action.evidence} shows ${cliName()} is active — suppressing submit warning. preview="${preview}"`);
4302
+ // For a durable turn, activity is evidence of possible submission, not
4303
+ // a terminal result. Keep the arbiter closed and re-check until either
4304
+ // the transcript confirms it or a quiet interval proves it stuck.
4305
+ if (turnIdentity?.dispatchAttempt !== undefined) {
4306
+ scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId, undefined, turnSeq, turnIdentity);
4307
+ }
3783
4308
  return;
3784
4309
  case 'notify-hard-failure':
3785
4310
  // failureReason is handled synchronously above.
@@ -3789,11 +4314,14 @@ function scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId
3789
4314
  }
3790
4315
  dropBridgeMark();
3791
4316
  log(`Deferred recheck still missing — notifying user. preview="${preview}"`);
3792
- send({
3793
- type: 'user_notify',
3794
- turnId: currentBotmuxTurnId,
3795
- message: t('worker.submit_unconfirmed', { cliName: cliName(), secs: Math.round(SUBMIT_DEFERRED_RECHECK_MS / 1000), transcriptLabel, preview }),
3796
- });
4317
+ emitDurableFailure('submit_unconfirmed');
4318
+ if (turnIdentity?.dispatchAttempt === undefined) {
4319
+ send({
4320
+ type: 'user_notify',
4321
+ turnId: turnIdentity?.turnId ?? currentBotmuxTurnId,
4322
+ message: t('worker.submit_unconfirmed', { cliName: cliName(), secs: Math.round(SUBMIT_DEFERRED_RECHECK_MS / 1000), transcriptLabel, preview }),
4323
+ });
4324
+ }
3797
4325
  }, SUBMIT_DEFERRED_RECHECK_MS);
3798
4326
  }
3799
4327
  /**
@@ -3856,7 +4384,7 @@ function detectBareShellLaunch() {
3856
4384
  `可能原因:rc 文件启动过慢/报错,或 \`${cli}\` 的可执行文件不在 PATH 上(CLI 没找到)。\n` +
3857
4385
  `建议:在 web 终端里手动敲一下启动命令看报什么错;确认 CLI 二进制能在 PATH 上找到;或精简 rc 启动逻辑后重启 daemon 再试。`;
3858
4386
  }
3859
- send({ type: 'user_notify', turnId: currentBotmuxTurnId, message });
4387
+ send({ type: 'user_notify', turnId: currentBotmuxTurnId, dispatchAttempt: currentBotmuxDispatchAttempt, message });
3860
4388
  return true;
3861
4389
  }
3862
4390
  /**
@@ -3866,6 +4394,11 @@ function detectBareShellLaunch() {
3866
4394
  * Messages pushed during a flush are picked up by the while loop.
3867
4395
  */
3868
4396
  async function flushPending() {
4397
+ // destroySession() may be asynchronous while `backend` still references the
4398
+ // old CLI. Never let a new flush (including one triggered by the old
4399
+ // backend's idle/task-done callback) write across that restart boundary.
4400
+ if (cliRestartInProgress)
4401
+ return;
3869
4402
  if (isFlushing)
3870
4403
  return; // while loop in active flush will pick up new messages
3871
4404
  if (!backend || !cliAdapter)
@@ -3876,8 +4409,25 @@ async function flushPending() {
3876
4409
  return; // wait for /rename to finish before any user input
3877
4410
  if (commandLineWritesPending > 0)
3878
4411
  return; // do not splice into text -> Enter
4412
+ // 注入进行中不得并发写 PTY(用户消息留在 pendingMessages,注入完成后的下一次
4413
+ // markPromptReady 自然排空)——防止 type-ahead 插进注入的 text→Enter 窗口。
4414
+ if (injectionFlushing)
4415
+ return;
4416
+ // cwd 切换是 barrier:在它执行前,任何用户消息都不得写入(type-ahead 路径也会
4417
+ // 走到这里,因为 supportsTypeAhead 的 CLI 从 sendToPty 直接调 flushPending,
4418
+ // 完全绕过 markPromptReady 的 barrier-first 分支)——否则消息(含 raw input 与
4419
+ // rename)会落进旧 cwd 的 CLI。消息留在 pendingMessages,待 markPromptReady →
4420
+ // flushPendingInjections 消费完 barrier 后由其 finally 的 re-kick 排空。
4421
+ if (shouldDeferUserFlush(pendingInjections))
4422
+ return;
3879
4423
  if (bareShellLaunchBlocked)
3880
4424
  return; // launch failed into a bare shell — don't type prompts into it
4425
+ // Screen-idle is not a durable receipt. A permission/AskUser prompt can look
4426
+ // idle while the logical delivery is unresolved, so no following IM or
4427
+ // meeting turn may cross this boundary until an explicit terminal releases
4428
+ // the active attempt.
4429
+ if (!pendingInputMayFlush(durableTurnInFlight))
4430
+ return;
3881
4431
  // Ready-gate: hold the FIRST prompt until the SessionStart hook fires a true-
3882
4432
  // ready signal. A cjadk-style startup selector's ❯ falsely matches readyPattern
3883
4433
  // and would otherwise eat this message. releaseReadyGate() re-invokes us once
@@ -3918,7 +4468,7 @@ async function flushPending() {
3918
4468
  // Codex on codex-cli 0.134.0.
3919
4469
  const claudeBridgeActive = !!bridgeJsonlPath && !lastInitConfig?.adoptMode;
3920
4470
  const codexBridgeActive = codexBridgeFallbackActive();
3921
- const typeAheadAllowed = cliAdapter.supportsTypeAhead;
4471
+ const typeAheadAllowed = pendingInputAllowsTypeAhead(cliAdapter.supportsTypeAhead === true, durableTurnInFlight, pendingMessages[0]);
3922
4472
  // Native /rename is an administrative command, not a steer/queued model
3923
4473
  // message. It must wait for a real prompt even on type-ahead CLIs. Normal
3924
4474
  // pending messages can still drain while busy; the rename stays queued.
@@ -4001,12 +4551,18 @@ async function flushPending() {
4001
4551
  }
4002
4552
  while (pendingMessages.length > 0 && backend && cliAdapter) {
4003
4553
  const item = pendingMessages.shift();
4554
+ const durableWrite = item.dispatchAttempt !== undefined;
4555
+ if (durableWrite)
4556
+ durableTurnInFlight = true;
4004
4557
  // Track as in-flight until the CLI returns to idle (markPromptReady).
4005
4558
  // If the CLI exits first, onExit stashes these for re-queue on respawn.
4006
4559
  inflightInputs.onWrite(item);
4007
4560
  const msg = item.content;
4008
4561
  currentBotmuxTurnId = item.turnId;
4562
+ currentBotmuxDispatchAttempt = item.dispatchAttempt;
4563
+ currentVcMeetingImTurnOrigin = item.vcMeetingImTurnOrigin;
4009
4564
  writeCliPidMarker();
4565
+ publishSandboxRelayCapability();
4010
4566
  const turnSeq = usageLimitTracker.beginTurn(currentUsageLimitSnapshot());
4011
4567
  // Bridge fallback: mark immediately before writeInput. Doing it here
4012
4568
  // (instead of at enqueue time) means markTimeMs anchors to the
@@ -4021,14 +4577,27 @@ async function flushPending() {
4021
4577
  bridgeIngest();
4022
4578
  }
4023
4579
  catch { /* best-effort */ }
4024
- bridgeTurnId = bridgeMarkPendingTurn(msg, item.turnId);
4580
+ bridgeTurnId = bridgeMarkPendingTurn(msg, item.turnId, item.dispatchAttempt);
4025
4581
  }
4026
4582
  else if (codexBridgeActive) {
4027
4583
  // Codex mark works even before the rollout path is known: the
4028
4584
  // queue is path-agnostic, and the late-attach below will start
4029
4585
  // ingest from offset 0 so the user_message that lands shortly
4030
4586
  // after still fingerprint-matches this turn.
4031
- codexBridgeMarkPendingTurn(msg, item.turnId);
4587
+ codexBridgeMarkPendingTurn(msg, item.turnId, item.dispatchAttempt);
4588
+ }
4589
+ if (durableWrite
4590
+ && cliAdapter.reliableTurnTerminal === true
4591
+ && lastInitConfig?.cliId === 'claude-code'
4592
+ && (!item.turnId || !bridgeTurnId)) {
4593
+ // The advertised Claude contract is transcript-backed. If we cannot
4594
+ // establish the mark before touching the PTY, fail without submitting;
4595
+ // otherwise this real delivery would have no attributable final edge.
4596
+ if (item.turnId) {
4597
+ emitTurnTerminal(item.turnId, 'failed', 'terminal_bridge_unavailable', item.dispatchAttempt);
4598
+ }
4599
+ log('Refused durable Claude submit: transcript terminal bridge is unavailable');
4600
+ break;
4032
4601
  }
4033
4602
  log(`Writing to PTY (flush): "${msg.substring(0, 80)}"`);
4034
4603
  // Defense in depth: TmuxPipeBackend's send methods no longer throw on a
@@ -4046,12 +4615,18 @@ async function flushPending() {
4046
4615
  }
4047
4616
  catch (err) {
4048
4617
  log(`writeInput threw: ${err?.message ?? err}`);
4618
+ if (durableWrite && item.turnId) {
4619
+ // A throwing backend cannot prove whether bytes reached the CLI.
4620
+ // Reconcile as ambiguous (not a definitive failure) so the receiver
4621
+ // can replay the same frozen delivery behind the action gate.
4622
+ emitTurnTerminal(item.turnId, 'ambiguous', 'write_input_threw', item.dispatchAttempt);
4623
+ }
4049
4624
  // If the CLI exited mid-write the backend already fired onExit (which
4050
4625
  // nulled `backend` and told the user the CLI exited) — nothing more to
4051
4626
  // do. Otherwise surface it as a submit failure so the message isn't
4052
4627
  // silently lost.
4053
4628
  if (backend)
4054
- scheduleSubmitFailureNotify(msg, undefined, '会话 JSONL', bridgeTurnId, undefined, turnSeq);
4629
+ scheduleSubmitFailureNotify(msg, undefined, '会话 JSONL', bridgeTurnId, undefined, turnSeq, item);
4055
4630
  break;
4056
4631
  }
4057
4632
  // Persist any sessionId the adapter observed via authoritative sources
@@ -4070,7 +4645,7 @@ async function flushPending() {
4070
4645
  // nulled backend) the user already got a "CLI exited" notice; don't also
4071
4646
  // nag that the submit wasn't confirmed.
4072
4647
  if (result && result.submitted === false && backend) {
4073
- scheduleSubmitFailureNotify(msg, result.recheck, '会话 JSONL', bridgeTurnId, result.failureReason, turnSeq);
4648
+ scheduleSubmitFailureNotify(msg, result.recheck, '会话 JSONL', bridgeTurnId, result.failureReason, turnSeq, item);
4074
4649
  }
4075
4650
  // All structured bridges now drain every pending message in one flush:
4076
4651
  // Claude's BridgeTurnQueue handles `attachment(queued_command)` events
@@ -4078,22 +4653,44 @@ async function flushPending() {
4078
4653
  // and writes the user event at dequeue time (transcript stays interleaved);
4079
4654
  // Codex parks them too but steers them into the active turn (which can
4080
4655
  // merge into one final), and CodexBridgeQueue's HOL-block-drop attributes
4081
- // that correctly. We WANT them to drain all pending here so the extras
4082
- // land in the TUI queue rather than waiting for the next idle.
4656
+ // that correctly. Durable receiver attempts are the exception: they and
4657
+ // adjacent IM turns wait for separate idle edges so neither can be
4658
+ // HOL-dropped or steered into the other.
4659
+ if (shouldStopPendingBatch(item, pendingMessages[0]))
4660
+ break;
4083
4661
  }
4084
4662
  }
4085
4663
  finally {
4086
4664
  isFlushing = false;
4087
4665
  }
4088
4666
  }
4089
- function sendToPty(content, turnId, codexAppInput) {
4090
- if (!backend || !cliAdapter)
4667
+ function sendToPty(content, turnId, opts = {}) {
4668
+ if (!cliAdapter)
4669
+ return;
4670
+ const next = {
4671
+ content,
4672
+ turnId,
4673
+ ...(opts.codexAppInput ? { codexAppInput: opts.codexAppInput } : {}),
4674
+ ...(opts.dispatchAttempt !== undefined ? { dispatchAttempt: opts.dispatchAttempt } : {}),
4675
+ ...(opts.vcMeetingImTurnOrigin
4676
+ ? { vcMeetingImTurnOrigin: opts.vcMeetingImTurnOrigin }
4677
+ : {}),
4678
+ };
4679
+ // During an exact lease-fenced CLI restart the worker stays alive while the
4680
+ // backend is rebuilt. Preserve incoming attempt N+1 in the worker queue; the
4681
+ // old early-return silently dropped it after receiver had already persisted
4682
+ // DISPATCHED.
4683
+ if (cliRestartInProgress || !backend) {
4684
+ pendingMessages.push(next);
4685
+ log(`Queued message while CLI backend is restarting (${pendingMessages.length} pending)`);
4091
4686
  return;
4092
- const next = { content, turnId, codexAppInput };
4093
- const shouldMergeQueued = !isFlushing && !shouldWriteNow({
4687
+ }
4688
+ const supportsTypeAhead = pendingInputAllowsTypeAhead(cliAdapter.supportsTypeAhead === true, durableTurnInFlight, next);
4689
+ const shouldMergeQueued = opts.dispatchAttempt === undefined && !durableTurnInFlight
4690
+ && !isFlushing && !shouldWriteNow({
4094
4691
  isPromptReady,
4095
4692
  isFlushing,
4096
- supportsTypeAhead: cliAdapter.supportsTypeAhead === true,
4693
+ supportsTypeAhead,
4097
4694
  awaitingFirstPrompt,
4098
4695
  }) && cliAdapter.mergeQueuedInput === true;
4099
4696
  const mergedQueued = shouldMergeQueued && mergeQueuedCliInput(pendingMessages, next);
@@ -4126,7 +4723,9 @@ function sendToPty(content, turnId, codexAppInput) {
4126
4723
  // type-ahead write is dropped (no input box yet) — markPromptReady()'s flush
4127
4724
  // delivers queued messages instead. See input-gate.ts; this fixes dispatch's
4128
4725
  // brief reaching Codex before its first idle and never landing.
4129
- if (!sessionRenameInFlight && commandLineWritesPending === 0 && shouldWriteNow({ isPromptReady, isFlushing, supportsTypeAhead: cliAdapter.supportsTypeAhead === true, awaitingFirstPrompt })) {
4726
+ if (!sessionRenameInFlight && commandLineWritesPending === 0 && shouldWriteNow({
4727
+ isPromptReady, isFlushing, supportsTypeAhead, awaitingFirstPrompt,
4728
+ })) {
4130
4729
  if (!mergedQueued)
4131
4730
  log(`Writing to PTY: "${content.substring(0, 80)}"`);
4132
4731
  flushPending(); // fire-and-forget async; no-op if already flushing
@@ -4209,7 +4808,7 @@ function startScreenUpdates() {
4209
4808
  const usageAware = usageLimitTracker.classify(content, status);
4210
4809
  if (changed || usageAware.status !== lastSentStatus) {
4211
4810
  lastSentStatus = usageAware.status;
4212
- send({ type: 'screen_update', content, ...usageAware, turnId: currentBotmuxTurnId });
4811
+ send({ type: 'screen_update', content, ...usageAware, turnId: currentBotmuxTurnId, dispatchAttempt: currentBotmuxDispatchAttempt });
4213
4812
  }
4214
4813
  })();
4215
4814
  }, SCREEN_UPDATE_INTERVAL_MS);
@@ -4424,8 +5023,12 @@ function setupAdoptIdleDetection(cfg, label) {
4424
5023
  function seedBackendScreen(source, be) {
4425
5024
  try {
4426
5025
  const initial = be.captureCurrentScreen?.() ?? '';
4427
- if (initial.length > 0)
5026
+ if (initial.length > 0) {
4428
5027
  onPtyData(initial);
5028
+ if (be instanceof HerdrBackend) {
5029
+ relayHerdrWebSnapshot(initial);
5030
+ }
5031
+ }
4429
5032
  }
4430
5033
  catch (err) {
4431
5034
  log(`${source} captureCurrentScreen failed: ${err.message}`);
@@ -4549,6 +5152,7 @@ function spawnCli(cfg) {
4549
5152
  rows,
4550
5153
  env: process.env,
4551
5154
  });
5155
+ wireHerdrWebTerminalRelays(herdrBe);
4552
5156
  seedBackendScreen('herdr adopt', herdrBe);
4553
5157
  setupAdoptTranscriptBridges(cfg);
4554
5158
  setupAdoptInputAdapter(cfg);
@@ -4562,7 +5166,7 @@ function spawnCli(cfg) {
4562
5166
  backend = null;
4563
5167
  isPromptReady = false;
4564
5168
  stopBridgeWatcher();
4565
- send({ type: 'claude_exit', code, signal });
5169
+ send({ type: 'claude_exit', code, signal, turnId: currentBotmuxTurnId, dispatchAttempt: currentBotmuxDispatchAttempt });
4566
5170
  });
4567
5171
  awaitingFirstPrompt = false;
4568
5172
  renderer?.markNewTurn();
@@ -4615,7 +5219,7 @@ function spawnCli(cfg) {
4615
5219
  backend = null;
4616
5220
  isPromptReady = false;
4617
5221
  stopBridgeWatcher();
4618
- send({ type: 'claude_exit', code, signal });
5222
+ send({ type: 'claude_exit', code, signal, turnId: currentBotmuxTurnId, dispatchAttempt: currentBotmuxDispatchAttempt });
4619
5223
  });
4620
5224
  awaitingFirstPrompt = false;
4621
5225
  renderer?.markNewTurn();
@@ -4852,6 +5456,7 @@ function spawnCli(cfg) {
4852
5456
  }
4853
5457
  }
4854
5458
  let isolationBotHome;
5459
+ let isolatedCodexHome;
4855
5460
  if (willReadIsolate) {
4856
5461
  isolationBotHome = ownBotHome;
4857
5462
  const isClaudeFam = !!claudeDataDir;
@@ -4871,6 +5476,17 @@ function spawnCli(cfg) {
4871
5476
  if (report.warning)
4872
5477
  log(`[mcp-gateway] WARN ${report.warning}`);
4873
5478
  }
5479
+ if (!isClaudeFam) {
5480
+ isolatedCodexHome = join(isolationBotHome, 'codex');
5481
+ // The CLI child and its dedicated worker must resolve the same Codex data
5482
+ // root. Adapter submit confirmation, resume fallback, and transcript bridge
5483
+ // discovery all run in the worker and consult process.env.CODEX_HOME
5484
+ // dynamically. Setting only childEnv made successful submissions look
5485
+ // unconfirmed because the worker kept watching the global ~/.codex history.
5486
+ // A worker owns one session, so this process-local redirect cannot leak
5487
+ // between bots or sessions.
5488
+ process.env.CODEX_HOME = isolatedCodexHome;
5489
+ }
4874
5490
  }
4875
5491
  // Predict reattach vs fresh BEFORE the resume pre-flight. On a persistent
4876
5492
  // backend (tmux/herdr/zellij) a daemon restart finds the CLI process still
@@ -4893,11 +5509,11 @@ function spawnCli(cfg) {
4893
5509
  // survive a daemon restart still running a CLI that may NOT be isolated (e.g.
4894
5510
  // spawned before isolation was enabled, or by an old build). Isolation is only
4895
5511
  // injectable at spawn time, so reattaching such a pane would silently run
4896
- // unisolated. We stamp a boot-id marker when we spawn an isolated pane; if this
4897
- // isolated bot's existing pane is NOT stamped by THIS daemon lifetime, kill it
4898
- // so the probe below sees no pane and we cold-spawn fresh isolated. A pane from
4899
- // this lifetime (suspend→resume) keeps its markerreattaches normally (it is
4900
- // still the isolated process). This lets isolated bots use tmux/zellij/herdr.
5512
+ // unisolated. We stamp a versioned policy marker when we spawn an isolated
5513
+ // pane; if an existing pane was not spawned under the current policy, kill it
5514
+ // so the probe below sees no pane and we cold-spawn fresh isolated. A current-
5515
+ // policy pane survives daemon restarts and suspendresume safely because the
5516
+ // confinement remains attached to the live process.
4901
5517
  if ((willReadIsolate || willWriteSandbox) && persistentSessionName && effectiveBackendType !== 'pty') {
4902
5518
  const paneLive = effectiveBackendType === 'tmux'
4903
5519
  ? TmuxBackend.hasSession(persistentSessionName)
@@ -4906,20 +5522,17 @@ function spawnCli(cfg) {
4906
5522
  : HerdrBackend.hasSession(persistentSessionName);
4907
5523
  if (paneLive) {
4908
5524
  let marker = null;
4909
- try {
4910
- marker = readFileSync(join(process.env.SESSION_DATA_DIR ?? '', 'read-isolation', `${cfg.sessionId}.boot`), 'utf-8');
4911
- }
4912
- catch { /* no marker → pane was spawned WITHOUT isolation */ }
5525
+ marker = readRegularHostFileNoFollow(join(process.env.SESSION_DATA_DIR ?? '', 'read-isolation', `${cfg.sessionId}.boot`));
4913
5526
  if (isolatedPaneReattachSafe(marker)) {
4914
- // Pane was spawned isolated (marker present) → still confined on the running
4915
- // process even across daemon restarts warm reattach is safe and preserves
5527
+ // Pane was spawned under the current isolation policy → still confined
5528
+ // on the running process across daemon restarts; warm reattach preserves
4916
5529
  // resume/context + tmux idle-suspend.
4917
5530
  log(`[read-isolation] reattaching isolated persistent pane (${cfg.sessionId})`);
4918
5531
  }
4919
5532
  else {
4920
- // No marker → pane predates isolation (or an old build) → could be running
4921
- // UNISOLATED kill it so the probe below cold-spawns fresh isolated.
4922
- log(`[read-isolation] unmarked persistent pane for ${cfg.sessionId} (not spawned isolated) — killing + cold-spawning isolated`);
5533
+ // Missing/legacy marker → pane predates the current policy and may retain
5534
+ // obsolete permissions. Kill it before publishing any new capability.
5535
+ log(`[read-isolation] legacy/unmarked persistent pane for ${cfg.sessionId} — killing + cold-spawning with current policy`);
4923
5536
  try {
4924
5537
  killPersistentSession(effectiveBackendType, persistentSessionName);
4925
5538
  }
@@ -5048,6 +5661,10 @@ function spawnCli(cfg) {
5048
5661
  hasStartupCommands: !!cfg.startupCommands?.length,
5049
5662
  adoptMode: cfg.adoptMode === true,
5050
5663
  passesInitialPromptViaArgs: cliAdapter.passesInitialPromptViaArgs === true,
5664
+ }) || shouldDeferArgsBakedDurablePrompt({
5665
+ passesInitialPromptViaArgs: cliAdapter.passesInitialPromptViaArgs === true,
5666
+ adoptMode: cfg.adoptMode === true,
5667
+ dispatchAttempt: cfg.dispatchAttempt,
5051
5668
  }) || (effectiveResume && cliAdapter.initialPromptArgsIgnoredOnResume === true);
5052
5669
  kiroSessionIdCaptureArmed = cfg.cliId === 'kiro-cli' && !effectiveCliSessionId && !willReattachPersistent;
5053
5670
  kiroSessionIdCaptureBuffer = '';
@@ -5064,10 +5681,22 @@ function spawnCli(cfg) {
5064
5681
  readIsolationCtx = {
5065
5682
  homeDir: homedir(),
5066
5683
  botmuxHome: dirname(sessionDataDir),
5684
+ defaultBotmuxHome: join(homedir(), '.botmux'),
5067
5685
  sessionDataDir,
5068
5686
  currentAppId: cfg.larkAppId,
5687
+ currentSessionId: cfg.sessionId,
5069
5688
  extraDenyPaths: cfg.readDenyExtraPaths,
5070
5689
  };
5690
+ readIsolationOriginCapabilityFile = process.platform === 'darwin'
5691
+ ? managedOriginCapabilityPath(sessionDataDir, cfg.sessionId)
5692
+ : null;
5693
+ // Replace any legacy child-planted symlink before Seatbelt canonicalizes
5694
+ // carve-outs. A failure is fatal: spawning with a missing/ambiguous
5695
+ // capability transport would either break all IPC or reopen an attacker-
5696
+ // selected realpath in the generated profile.
5697
+ if (readIsolationOriginCapabilityFile) {
5698
+ publishSandboxRelayCapability({ failClosed: true });
5699
+ }
5071
5700
  // Write this bot's OWN send-credential into its BOT_HOME (the same per-bot
5072
5701
  // private storage as its CLI data; siblings' BOT_HOMEs are whole-denied).
5073
5702
  // `botmux send` reads the secret from here instead of bots.json — so the
@@ -5198,6 +5827,10 @@ function spawnCli(cfg) {
5198
5827
  // approver allowlist against session.owner. Missing env → exit 2.
5199
5828
  childEnv.BOTMUX_SESSION_ID = cfg.sessionId;
5200
5829
  childEnv.BOTMUX_CHAT_ID = cfg.chatId;
5830
+ if (cfg.chatType)
5831
+ childEnv.BOTMUX_CHAT_TYPE = cfg.chatType;
5832
+ else
5833
+ delete childEnv.BOTMUX_CHAT_TYPE;
5201
5834
  childEnv.BOTMUX_LARK_APP_ID = cfg.larkAppId;
5202
5835
  childEnv.BOTMUX_ROOT_MESSAGE_ID = cfg.rootMessageId;
5203
5836
  // NOTE: under read isolation `botmux send` gets this bot's secret from the worker-
@@ -5216,6 +5849,9 @@ function spawnCli(cfg) {
5216
5849
  // Initial value only; long-lived panes get the latest turn via the JSON pid marker.
5217
5850
  if (cfg.turnId)
5218
5851
  childEnv.BOTMUX_TURN_ID = cfg.turnId;
5852
+ if (cfg.dispatchAttempt !== undefined) {
5853
+ childEnv.BOTMUX_DISPATCH_ATTEMPT = String(cfg.dispatchAttempt);
5854
+ }
5219
5855
  if (injectClaudeSandbox)
5220
5856
  childEnv.IS_SANDBOX = '1';
5221
5857
  if (claudeResumeTokenThreshold)
@@ -5235,7 +5871,7 @@ function spawnCli(cfg) {
5235
5871
  if (claudeDataDir)
5236
5872
  childEnv.CLAUDE_CONFIG_DIR = claudeDataDir; // = <BOT_HOME>/claude
5237
5873
  else
5238
- childEnv.CODEX_HOME = join(isolationBotHome, 'codex');
5874
+ childEnv.CODEX_HOME = isolatedCodexHome;
5239
5875
  }
5240
5876
  // Per-bot env (bots.json `env`): extra vars for THIS bot's CLI only — e.g.
5241
5877
  // ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN to run a bot on GLM/a 3rd-party
@@ -5283,18 +5919,25 @@ function spawnCli(cfg) {
5283
5919
  // readable ~/.botmux + system creds; the own slice is re-opened via carve-outs.
5284
5920
  // For a WRITE-sandbox-only session these stay empty → reads wide open (Linux parity).
5285
5921
  let denyPaths = [], denyRegexes = [], allowPaths = [], finalDenyPaths = [], traverseDirs = [];
5922
+ let protectedWrites;
5286
5923
  if (readIsolationCtx) {
5287
5924
  const profileCtx = {
5288
5925
  ...readIsolationCtx,
5289
5926
  homeDir: canonical(readIsolationCtx.homeDir),
5290
5927
  botmuxHome: canonical(readIsolationCtx.botmuxHome),
5928
+ defaultBotmuxHome: canonical(readIsolationCtx.defaultBotmuxHome ?? join(readIsolationCtx.homeDir, '.botmux')),
5291
5929
  sessionDataDir: canonical(readIsolationCtx.sessionDataDir),
5292
5930
  };
5293
5931
  denyPaths = buildV2DenyPaths(profileCtx).map(canonical);
5294
5932
  denyRegexes = buildV2DenyRegexes(profileCtx);
5295
5933
  const carve = buildV2CarveOuts(profileCtx);
5934
+ const capabilityCarvePath = managedOriginCapabilityPath(profileCtx.sessionDataDir, profileCtx.currentSessionId);
5296
5935
  allowPaths = [
5297
- ...carve.allowPaths.map(canonical),
5936
+ // Never realpath the capability leaf: another legacy sandbox can race
5937
+ // a symlink into that shared host directory while multiple persistent
5938
+ // sessions recover. Keeping the exact path may fail closed on a raced
5939
+ // symlink, but can never turn its target into a Seatbelt allow rule.
5940
+ ...carve.allowPaths.map(path => path === capabilityCarvePath ? path : canonical(path)),
5298
5941
  ...buildCliExecutableReadCarveOuts({
5299
5942
  homeDir: profileCtx.homeDir,
5300
5943
  cliId: cliAdapter.id,
@@ -5303,6 +5946,16 @@ function spawnCli(cfg) {
5303
5946
  ];
5304
5947
  finalDenyPaths = carve.finalDenyPaths.map(canonical);
5305
5948
  traverseDirs = carve.traverseDirs.map(canonical);
5949
+ protectedWrites = buildReadIsolationProtectedWriteRules(profileCtx, {
5950
+ // Keep lexical roots as well as their canonical targets. Canonical
5951
+ // rules protect contents reached through a symlink; lexical literals
5952
+ // prevent replacing the symlink/root directory entry itself.
5953
+ dashboardRoots: [
5954
+ readIsolationCtx.defaultBotmuxHome ?? join(readIsolationCtx.homeDir, '.botmux'),
5955
+ readIsolationCtx.botmuxHome,
5956
+ ],
5957
+ sessionDataDirs: [readIsolationCtx.sessionDataDir],
5958
+ });
5306
5959
  }
5307
5960
  // WRITE rules — the macOS file-sandbox (Linux-bwrap twin). Realpath the writable
5308
5961
  // zones + crown-jewel re-denies for the same symlink-safety reason as reads.
@@ -5319,21 +5972,21 @@ function spawnCli(cfg) {
5319
5972
  const profileDir = join(process.env.SESSION_DATA_DIR, 'read-isolation');
5320
5973
  mkdirSync(profileDir, { recursive: true });
5321
5974
  const profilePath = join(profileDir, `${cfg.sessionId}.sb`);
5322
- writeFileSync(profilePath, buildSeatbeltProfile(denyPaths, allowPaths, finalDenyPaths, traverseDirs, denyRegexes, writeRules), { mode: 0o600 });
5975
+ replaceManagedOriginCapabilityFile(profilePath, buildSeatbeltProfile(denyPaths, allowPaths, finalDenyPaths, traverseDirs, denyRegexes, writeRules, protectedWrites));
5323
5976
  seatbeltProfilePath = profilePath;
5324
5977
  spawnArgs = ['-f', profilePath, spawnBin, ...spawnArgs];
5325
5978
  spawnBin = 'sandbox-exec';
5326
5979
  log(`[file-sandbox] wrapping ${cliAdapter.id} in Seatbelt (read-isolation=${!!readIsolationCtx}, write-sandbox=${!!writeRules}): sandbox-exec -f ${profilePath}`);
5327
5980
  }
5328
5981
  // Fresh sandboxed spawn on a persistent backend: stamp the pane with this daemon's
5329
- // boot id so a later suspend→resume reattach can be trusted (see the stale-pane
5982
+ // policy version + boot id so a later suspend→resume reattach can be trusted (see the stale-pane
5330
5983
  // guard above). Applies to read-isolation AND write-sandbox panes (both carry the
5331
5984
  // Seatbelt confinement on the live process). pty needs no marker (never reattached).
5332
5985
  if ((readIsolationCtx || writeSandboxRules) && persistentSessionName && !willReattachPersistent) {
5333
5986
  try {
5334
5987
  const markerDir = join(process.env.SESSION_DATA_DIR, 'read-isolation');
5335
5988
  mkdirSync(markerDir, { recursive: true });
5336
- writeFileSync(join(markerDir, `${cfg.sessionId}.boot`), cfg.daemonBootId ?? '', { mode: 0o600 });
5989
+ replaceManagedOriginCapabilityFile(join(markerDir, `${cfg.sessionId}.boot`), isolationPaneMarkerContent(cfg.daemonBootId ?? ''));
5337
5990
  }
5338
5991
  catch { /* non-fatal: worst case a same-lifetime reattach cold-spawns instead */ }
5339
5992
  }
@@ -5373,6 +6026,7 @@ function spawnCli(cfg) {
5373
6026
  ...readIsolationCtx,
5374
6027
  homeDir: canon(readIsolationCtx.homeDir),
5375
6028
  botmuxHome: canon(readIsolationCtx.botmuxHome),
6029
+ defaultBotmuxHome: canon(readIsolationCtx.defaultBotmuxHome ?? join(readIsolationCtx.homeDir, '.botmux')),
5376
6030
  sessionDataDir: canon(readIsolationCtx.sessionDataDir),
5377
6031
  };
5378
6032
  const ids = new Set();
@@ -5454,7 +6108,9 @@ function spawnCli(cfg) {
5454
6108
  catch { /* */ }
5455
6109
  }
5456
6110
  sandboxCleanup = att.cleanup;
5457
- sandboxStopWatcher = startOutboxWatcher(att.outbox, childEnv, cfg.sessionId);
6111
+ sandboxRelayOutbox = att.outbox;
6112
+ sandboxStopWatcher = startOutboxWatcher(att.outbox, childEnv, cfg.sessionId, { authorize: authorizeManagedSend });
6113
+ publishSandboxRelayCapability();
5458
6114
  log(`Sandbox REATTACH (${cfg.cliId}): live pane CLI kept, re-wired outbox=${att.outbox} (no remount)`);
5459
6115
  }
5460
6116
  else {
@@ -5477,7 +6133,15 @@ function spawnCli(cfg) {
5477
6133
  // sensitive set, keep the own BOT_HOME real+writable (authPaths), and re-
5478
6134
  // expose the own attachments bucket read-only after the masks (readonlyRoots).
5479
6135
  hidePaths: [...(cfg.sandboxHidePaths ?? []), ...(readIsoLinuxMasks?.hidePaths ?? [])],
5480
- authPaths: [...(cliAdapter.authPaths ?? []), ...(readIsoLinuxMasks?.ownReadWritePaths ?? [])],
6136
+ authPaths: [...(cliAdapter.authPaths ?? [])],
6137
+ // BOTMUX_HOME is credential-masked wholesale. Re-open only this
6138
+ // bot's daemon-derived private CLI home after that parent mask, then
6139
+ // re-mask its send credential so an untrusted receiver cannot bypass
6140
+ // the host-authorized relay with a direct Lark API call.
6141
+ trustedWritablePaths: readIsoLinuxMasks?.ownReadWritePaths ?? [],
6142
+ finalHidePaths: readIsoLinuxMasks
6143
+ ? [sendCredFilePath(dataDir, cfg.larkAppId)]
6144
+ : [],
5481
6145
  extraExecPaths: cliAdapter.sandboxExtraExecPaths?.(),
5482
6146
  readonlyRoots: [...(cfg.skillReadonlyRoots ?? []), ...(readIsoLinuxMasks?.ownReadOnlyPaths ?? [])],
5483
6147
  userReadonlyPaths: cfg.sandboxReadonlyPaths ?? [],
@@ -5503,8 +6167,10 @@ function spawnCli(cfg) {
5503
6167
  catch { /* */ }
5504
6168
  }
5505
6169
  sandboxCleanup = sbx.cleanup;
6170
+ sandboxRelayOutbox = sbx.outbox;
5506
6171
  // session-id is FORCED here so a relayed send can't target another session.
5507
- sandboxStopWatcher = startOutboxWatcher(sbx.outbox, childEnv, cfg.sessionId);
6172
+ sandboxStopWatcher = startOutboxWatcher(sbx.outbox, childEnv, cfg.sessionId, { authorize: authorizeManagedSend });
6173
+ publishSandboxRelayCapability();
5508
6174
  log(`Sandbox ON (${cfg.cliId}): upper=${sbx.workDir} outbox=${sbx.outbox}`);
5509
6175
  }
5510
6176
  else {
@@ -5862,6 +6528,7 @@ function spawnCli(cfg) {
5862
6528
  });
5863
6529
  }
5864
6530
  backend.onData(onPtyData);
6531
+ const observedBackend = backend;
5865
6532
  backend.onAccessUrl?.((url) => {
5866
6533
  send({ type: 'riff_access_url', accessUrl: url });
5867
6534
  });
@@ -5877,8 +6544,57 @@ function spawnCli(cfg) {
5877
6544
  backend.onTaskId?.((taskId) => {
5878
6545
  send({ type: 'riff_task_id', taskId });
5879
6546
  });
6547
+ if (backend instanceof HerdrBackend) {
6548
+ wireHerdrWebTerminalRelays(backend);
6549
+ restoreHerdrWebBindings();
6550
+ }
5880
6551
  backend.onExit((code, signal) => {
6552
+ const intentionalRestart = intentionalRestartBackend === observedBackend;
6553
+ if (intentionalRestart)
6554
+ intentionalRestartBackend = null;
6555
+ // destroySession()/kill() may report exit after the 500ms replacement spawn
6556
+ // has already installed a fresh backend. That callback belongs to the old
6557
+ // generation: apart from consuming its intentional-restart marker, it must
6558
+ // not clear the replacement backend/durable HOL state or emit claude_exit.
6559
+ if (backend !== observedBackend) {
6560
+ log(`Ignored stale backend exit (code: ${code}, signal: ${signal})`);
6561
+ return;
6562
+ }
6563
+ const exitedTurnId = currentBotmuxTurnId;
6564
+ const exitedDispatchAttempt = currentBotmuxDispatchAttempt;
6565
+ // Fail closed as soon as this CLI generation ends. The Node worker may
6566
+ // stay alive for auto-restart/crash diagnostics, but an old sandbox relay
6567
+ // token or explicit IM origin must not remain usable in that interval.
6568
+ completeManagedTurnOriginRevocation(sandboxRelayCapability, exitedTurnId, exitedDispatchAttempt);
5881
6569
  log(`${cliName()} exited (code: ${code}, signal: ${signal})`);
6570
+ if (cliAdapter?.reliableTurnTerminal === true
6571
+ && exitedTurnId
6572
+ && exitedDispatchAttempt !== undefined) {
6573
+ // The CLI may have durably appended its terminal record immediately
6574
+ // before exiting while fs.watch/the 1s poller is still queued. Drain it
6575
+ // synchronously before claiming `cli_exit`; otherwise ambiguous wins the
6576
+ // deduper and needlessly replays a turn that actually completed.
6577
+ if (bridgeJsonlPath) {
6578
+ try {
6579
+ bridgeDrainAndMaybeEmit();
6580
+ }
6581
+ catch (err) {
6582
+ log(`Bridge exit drain failed: ${err.message}`);
6583
+ }
6584
+ }
6585
+ if (codexBridgeFallbackActive()) {
6586
+ try {
6587
+ codexBridgeDrainAndMaybeEmit({ signalIdle: false });
6588
+ }
6589
+ catch (err) {
6590
+ log(`Codex bridge exit drain failed: ${err.message}`);
6591
+ }
6592
+ }
6593
+ // Race-safe with transcript final / submit-failure: the worker-local
6594
+ // terminal deduper lets exactly one status win for this attempt.
6595
+ emitTurnTerminal(exitedTurnId, 'ambiguous', 'cli_exit', exitedDispatchAttempt);
6596
+ }
6597
+ durableTurnInFlight = false;
5882
6598
  const logTail = recentTerminalLogTail();
5883
6599
  // Don't park a diagnostic shell here: most exits are immediately
5884
6600
  // auto-restarted by the daemon, so an inline park would just be torn down
@@ -5893,13 +6609,24 @@ function spawnCli(cfg) {
5893
6609
  // the CLI — codex crashing mid-submit never records them, and the fresh
5894
6610
  // respawn comes up empty. Stash them so the next spawnCli re-queues and
5895
6611
  // re-delivers.
5896
- const stashed = inflightInputs.onCliExit();
6612
+ // Durable meeting replay has exactly one owner: the receiver receipt loop.
6613
+ // onCliExit tells the daemon to mark that receipt ambiguous; replaying the
6614
+ // same attempt locally as ordinary carry-over would race hub attempt N+1
6615
+ // and execute the prompt twice.
6616
+ const stashed = inflightInputs.onCliExit(item => item.dispatchAttempt === undefined);
5897
6617
  if (stashed > 0) {
5898
6618
  log(`CLI exited with ${stashed} in-flight message(s); will re-queue after restart`);
5899
6619
  }
5900
6620
  backend = null;
5901
6621
  isPromptReady = false;
5902
- send({ type: 'claude_exit', code, signal, logTail, canParkDiagnostic });
6622
+ currentBotmuxTurnId = undefined;
6623
+ currentBotmuxDispatchAttempt = undefined;
6624
+ if (intentionalRestart) {
6625
+ log('Suppressed claude_exit for intentional in-worker restart');
6626
+ }
6627
+ else {
6628
+ send({ type: 'claude_exit', code, signal, logTail, canParkDiagnostic, turnId: exitedTurnId, dispatchAttempt: exitedDispatchAttempt });
6629
+ }
5903
6630
  });
5904
6631
  if (isPipeMode && backend && 'isReattach' in backend && backend.isReattach) {
5905
6632
  log(`Re-attached to existing ${effectiveBackendType} session via pipe backend: ${persistentSessionName}`);
@@ -5955,7 +6682,7 @@ function spawnCli(cfg) {
5955
6682
  markPromptReady();
5956
6683
  }
5957
6684
  }
5958
- function killCli() {
6685
+ function killCli(opts = {}) {
5959
6686
  destroyCrashDiagnosticTerminal('killCli');
5960
6687
  idleDetector?.dispose();
5961
6688
  idleDetector = null;
@@ -5989,6 +6716,7 @@ function killCli() {
5989
6716
  catch { /* already gone */ }
5990
6717
  cliPidMarker = null;
5991
6718
  }
6719
+ completeManagedTurnOriginRevocation(sandboxRelayCapability, currentBotmuxTurnId, currentBotmuxDispatchAttempt);
5992
6720
  // Stop the sandbox outbox watcher, then unmount the overlays + remove the
5993
6721
  // per-session sandbox tree. In the overlay model the upper layer (the
5994
6722
  // changeset) must be landed BEFORE close — `/land` runs while the session is
@@ -6000,6 +6728,11 @@ function killCli() {
6000
6728
  catch { /* */ }
6001
6729
  sandboxStopWatcher = null;
6002
6730
  }
6731
+ sandboxRelayOutbox = null;
6732
+ readIsolationOriginCapabilityFile = null;
6733
+ currentBotmuxTurnId = undefined;
6734
+ currentBotmuxDispatchAttempt = undefined;
6735
+ currentVcMeetingImTurnOrigin = undefined;
6003
6736
  if (sandboxCleanup) {
6004
6737
  try {
6005
6738
  sandboxCleanup();
@@ -6009,15 +6742,112 @@ function killCli() {
6009
6742
  }
6010
6743
  isPromptReady = false;
6011
6744
  clearSessionRenameInFlight();
6012
- pendingMessages.length = 0;
6745
+ if (!opts.preservePending)
6746
+ pendingMessages.length = 0;
6013
6747
  // pendingRawInputs contains only commands that were accepted but never typed
6014
- // because /rename owned the TUI. Preserve them across restart; unlike an
6015
- // in-flight raw command, replaying these cannot duplicate a side effect.
6748
+ // because /rename owned the TUI or an owned CLI restart was already fenced.
6749
+ // Preserve them across restart; unlike an in-flight raw command, replaying
6750
+ // these cannot duplicate a side effect.
6751
+ // pendingInjections 则无条件清空(不随 preservePending 保留):barrier /cd 的
6752
+ // 目录变更已固化进 lastInitConfig.workingDir 与 daemon 记录,respawn 本身就落在
6753
+ // 新目录,重放 /cd 反而多余;非 barrier 注入(如 /compact)是 best-effort,
6754
+ // 不跨进程重放。
6755
+ pendingInjections.length = 0;
6016
6756
  scrollback = '';
6757
+ herdrWebHistory = null;
6758
+ herdrWebScrollDirection = null;
6759
+ herdrWebCursor = null;
6017
6760
  altBufferActive = false;
6018
6761
  trustHandled = false;
6019
6762
  codexUpdateDialogGuard.reset();
6020
- codexAppOscPending = '';
6763
+ appRunnerControlDecoder.reset();
6764
+ }
6765
+ async function restartCliProcess(reason, opts = {}) {
6766
+ if (lastInitConfig?.adoptMode) {
6767
+ log(`Restart ignored in adopt mode (${reason})`);
6768
+ return;
6769
+ }
6770
+ // Set before touching destroySession(): remote teardown can await for many
6771
+ // seconds while the old backend object is still non-null and still capable
6772
+ // of firing idle/task-done callbacks. Inputs accepted in that interval must
6773
+ // remain queued until a replacement backend has been installed.
6774
+ cliRestartInProgress = true;
6775
+ rawInputRestartGate = true;
6776
+ // The Node worker stays alive through this restart, so the daemon will see
6777
+ // neither claude_exit nor worker-exit. Explicitly revoke the old turn's
6778
+ // authority now, before jitter/async teardown leaves a stale-send window.
6779
+ // This deliberately preserves currentBotmuxTurnId/dispatchAttempt for the
6780
+ // old backend's terminal attribution.
6781
+ revokeManagedTurnOriginForRestart();
6782
+ log(`Restart requested (${reason})`);
6783
+ // Tier-2 guard: 2nd consecutive in-worker restart forces FRESH. Tier-1
6784
+ // adapter probing is still re-run on every spawn.
6785
+ consecutiveInWorkerRestarts++;
6786
+ log(`Restart count: ${consecutiveInWorkerRestarts} (>=2 forces FRESH)`);
6787
+ const restart = async () => {
6788
+ try {
6789
+ tmuxRestartTimer = null;
6790
+ const restartingBackend = backend;
6791
+ if (restartingBackend)
6792
+ intentionalRestartBackend = restartingBackend;
6793
+ // Riff teardown cancels a remote task asynchronously. Wait for the cancel
6794
+ // (bounded) before respawning, otherwise the old and replacement tasks can
6795
+ // overlap and both emit into the same Lark thread. Local backends normally
6796
+ // return void here and continue synchronously. A synchronous throw is
6797
+ // handled by the same fatal restart path below, so it cannot leave the
6798
+ // input gate permanently armed.
6799
+ const teardown = restartingBackend?.destroySession?.();
6800
+ if (teardown && typeof teardown.then === 'function') {
6801
+ try {
6802
+ await Promise.race([
6803
+ teardown,
6804
+ new Promise(resolve => setTimeout(resolve, 22_000)),
6805
+ ]);
6806
+ }
6807
+ catch { /* destroySession logs its own failure details */ }
6808
+ }
6809
+ killCli({ preservePending: opts.preservePending });
6810
+ awaitingFirstPrompt = true;
6811
+ setTimeout(async () => {
6812
+ if (lastInitConfig) {
6813
+ startScreenUpdates();
6814
+ startScreenAnalyzer();
6815
+ try {
6816
+ spawnCli({ ...lastInitConfig, resume: true, prompt: '' });
6817
+ }
6818
+ catch (err) {
6819
+ cliRestartInProgress = false;
6820
+ await sendFatalWorkerErrorAndExit(err);
6821
+ return;
6822
+ }
6823
+ }
6824
+ cliRestartInProgress = false;
6825
+ // Riff marks itself prompt-ready inside spawnCli(); that early flush is
6826
+ // intentionally held by the restart gate above. Release its raw-input
6827
+ // fence now; other backends keep it until their later markPromptReady().
6828
+ if (effectiveBackendType === 'riff' && isPromptReady)
6829
+ releaseRawInputRestartGate();
6830
+ void flushPending();
6831
+ }, 500);
6832
+ }
6833
+ catch (err) {
6834
+ cliRestartInProgress = false;
6835
+ try {
6836
+ await sendFatalWorkerErrorAndExit(err);
6837
+ }
6838
+ catch { /* sendFatalWorkerErrorAndExit is already best-effort */ }
6839
+ }
6840
+ };
6841
+ if (effectiveBackendType === 'tmux' && !opts.immediate) {
6842
+ const delayMs = tmuxRestartJitterMs(lastInitConfig?.sessionId ?? '', consecutiveInWorkerRestarts);
6843
+ log(`Staggering tmux teardown/restart by ${delayMs}ms to avoid shared-server probe storms`);
6844
+ if (tmuxRestartTimer)
6845
+ clearTimeout(tmuxRestartTimer);
6846
+ tmuxRestartTimer = setTimeout(() => { void restart(); }, delayMs);
6847
+ }
6848
+ else {
6849
+ await restart();
6850
+ }
6021
6851
  }
6022
6852
  // ─── HTTP + WebSocket Server ─────────────────────────────────────────────────
6023
6853
  function startWebServer(host, preferredPort) {
@@ -6030,17 +6860,47 @@ function startWebServer(host, preferredPort) {
6030
6860
  res.end('Bad Request');
6031
6861
  return;
6032
6862
  }
6033
- const tokenMatches = url.searchParams.get('token') === writeToken;
6034
- const { hasWrite, platformReadonly } = resolveTerminalWriteForReq(req, tokenMatches);
6863
+ const { hasRead, hasWrite, platformReadonly } = resolveTerminalAccessForReq(req, url);
6864
+ if (!hasRead) {
6865
+ res.writeHead(403, {
6866
+ 'Content-Type': 'text/plain; charset=utf-8',
6867
+ 'Cache-Control': 'no-store',
6868
+ });
6869
+ res.end('Forbidden');
6870
+ return;
6871
+ }
6035
6872
  const loginHdr = req.headers['x-botmux-login-url'];
6036
6873
  const loginUrl = typeof loginHdr === 'string' && /^https?:\/\/[^"'<>\s]+$/.test(loginHdr) ? loginHdr : '';
6874
+ // Herdr snapshots contain screen cells but not terminal mode state. On a
6875
+ // refreshed page an alt-screen CLI would otherwise look like an empty
6876
+ // normal buffer until resize causes a redraw. Preserve that mode so
6877
+ // scroll gestures continue to target the CLI's own transcript.
6878
+ const forceRemoteScroll = effectiveBackendType === 'herdr' && cliAdapter?.altScreen === true;
6037
6879
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
6038
- res.end(getTerminalHtml(hasWrite, platformReadonly, loginUrl));
6880
+ res.end(getTerminalHtml(hasWrite, platformReadonly, loginUrl, forceRemoteScroll));
6881
+ });
6882
+ wss = new WebSocketServer({
6883
+ server: httpServer,
6884
+ // Reject before the WebSocket handshake completes. Closing from the
6885
+ // `connection` callback is too late: an unauthenticated localhost scanner
6886
+ // briefly becomes a client and races the terminal history seed.
6887
+ verifyClient: ({ req }, done) => {
6888
+ const url = parseWorkerRequestUrl(req);
6889
+ if (!url) {
6890
+ done(false, 400, 'Bad Request');
6891
+ return;
6892
+ }
6893
+ if (!resolveTerminalAccessForReq(req, url).hasRead) {
6894
+ done(false, 403, 'Forbidden');
6895
+ return;
6896
+ }
6897
+ done(true);
6898
+ },
6039
6899
  });
6040
- wss = new WebSocketServer({ server: httpServer });
6041
6900
  wss.on('connection', (ws, req) => {
6042
6901
  wsClients.add(ws);
6043
- // Check token from query string for write access
6902
+ // Read access was already checked before the WebSocket handshake. Resolve
6903
+ // again here only to decide whether this client may write.
6044
6904
  const url = parseWorkerRequestUrl(req);
6045
6905
  if (!url) {
6046
6906
  log(`Bad worker WS URL rejected: ${JSON.stringify(req.url ?? '')}`);
@@ -6048,8 +6908,7 @@ function startWebServer(host, preferredPort) {
6048
6908
  ws.close(1008, 'Bad Request');
6049
6909
  return;
6050
6910
  }
6051
- const tokenMatches = url.searchParams.get('token') === writeToken;
6052
- const { hasWrite } = resolveTerminalWriteForReq(req, tokenMatches);
6911
+ const { hasWrite } = resolveTerminalAccessForReq(req, url);
6053
6912
  if (hasWrite)
6054
6913
  authedClients.add(ws);
6055
6914
  log(`WS client connected (total: ${wsClients.size}, write: ${hasWrite})`);
@@ -6078,7 +6937,13 @@ function startWebServer(host, preferredPort) {
6078
6937
  const startAttach = (cols, rows) => {
6079
6938
  if (cp)
6080
6939
  return;
6081
- cp = pty.spawn('tmux', ['attach-session', '-t', tmuxTarget], {
6940
+ // Defense in depth: view-capability clients attach through tmux's
6941
+ // own read-only mode as well as the WebSocket input gate below.
6942
+ cp = pty.spawn('tmux', [
6943
+ 'attach-session',
6944
+ ...(!hasWrite ? ['-r'] : []),
6945
+ '-t', tmuxTarget,
6946
+ ], {
6082
6947
  name: 'xterm-256color',
6083
6948
  cols,
6084
6949
  rows,
@@ -6115,13 +6980,11 @@ function startWebServer(host, preferredPort) {
6115
6980
  }
6116
6981
  }
6117
6982
  else if (msg.type === 'input' && typeof msg.data === 'string') {
6118
- if (!authedClients.has(ws)) {
6119
- // Read-only: allow mouse events through (scroll/click are
6120
- // non-destructive in tmux just views history / selects text).
6121
- // SGR mouse: \x1b[<... X10 mouse: \x1b[M...
6122
- if (!/^\x1b\[([<M])/.test(msg.data))
6123
- return;
6124
- }
6983
+ // Mouse protocols carry clicks, releases, drags and wheel input;
6984
+ // a mouse-aware TUI may bind any of them to approvals/actions.
6985
+ // A view capability therefore forwards no input bytes at all.
6986
+ if (!authedClients.has(ws))
6987
+ return;
6125
6988
  if (cp)
6126
6989
  cp.write(msg.data);
6127
6990
  else
@@ -6203,11 +7066,8 @@ function startWebServer(host, preferredPort) {
6203
7066
  cp.resize(msg.cols, msg.rows);
6204
7067
  }
6205
7068
  else if (msg.type === 'input' && typeof msg.data === 'string') {
6206
- if (!authedClients.has(ws)) {
6207
- // Read-only: only let mouse events (scroll/select) through.
6208
- if (!/^\x1b\[([<M])/.test(msg.data))
6209
- return;
6210
- }
7069
+ if (!authedClients.has(ws))
7070
+ return;
6211
7071
  if (cp)
6212
7072
  cp.write(msg.data);
6213
7073
  else
@@ -6235,6 +7095,12 @@ function startWebServer(host, preferredPort) {
6235
7095
  }
6236
7096
  else {
6237
7097
  // ── Shared relay (PtyBackend OR tmux pipe mode) ──
7098
+ const herdrWebBinding = new HerdrWebTerminalBinding(ws, () => (backend instanceof HerdrBackend && !lastInitConfig?.adoptMode ? backend : null));
7099
+ herdrWebBindings.set(ws, herdrWebBinding);
7100
+ const initialHerdrSize = herdrWebBinding.sync().initialSize;
7101
+ if (initialHerdrSize) {
7102
+ ws.send(`\x1b]1989;follower;${initialHerdrSize.cols};${initialHerdrSize.rows}\x07`);
7103
+ }
6238
7104
  // History seed: prefer tmux's authoritative capture-pane in pipe mode
6239
7105
  // (clean grid + scrollback) over replaying the raw cumulative byte
6240
7106
  // stream, which scrolls stale Ink redraw/spinner frames into scrollback
@@ -6252,29 +7118,37 @@ function startWebServer(host, preferredPort) {
6252
7118
  if (sz && sz.cols > 0 && sz.rows > 0)
6253
7119
  ws.send(`\x1b]1989;${sz.cols};${sz.rows}\x07`);
6254
7120
  }
6255
- const seed = chooseWebTerminalSeed({
6256
- canCapture: isPipeMode && isObserveBackend(backend),
6257
- capture: () => backend.captureCurrentScreen(),
6258
- scrollback,
6259
- onError: log,
6260
- });
7121
+ const seed = usesHerdrSnapshotWebHistory() && scrollback.length > 0
7122
+ ? scrollback
7123
+ : chooseWebTerminalSeed({
7124
+ canCapture: isPipeMode && isObserveBackend(backend),
7125
+ capture: () => backend.captureCurrentScreen(),
7126
+ scrollback,
7127
+ onError: log,
7128
+ });
6261
7129
  if (seed.length > 0) {
6262
- ws.send(seed);
7130
+ ws.send(seed + herdrWebCursorSequence());
6263
7131
  }
6264
7132
  ws.on('message', (raw) => {
6265
7133
  try {
6266
7134
  const msg = JSON.parse(String(raw));
6267
7135
  if (msg.type === 'resize' && msg.cols > 0 && msg.rows > 0) {
6268
- backend?.resize(msg.cols, msg.rows);
7136
+ const result = herdrWebBinding.resize(msg.cols, msg.rows);
7137
+ applyHerdrWebBindingResult(ws, result);
7138
+ if (!result.backend) {
7139
+ backend?.resize(msg.cols, msg.rows);
7140
+ }
6269
7141
  }
6270
7142
  else if (msg.type === 'input' && typeof msg.data === 'string') {
6271
- if (!authedClients.has(ws)) {
6272
- // Read-only: allow ONLY wheel scroll sequences (SGR buttons 64-67).
6273
- // Scrolling the CLI's own view is non-destructive and lets read-only
6274
- // viewers page back through an alt-screen TUI's history (Claude etc.,
6275
- // which has no local scrollback). Everything else is dropped.
6276
- if (!/^(\x1b\[<6[4-7];\d+;\d+M)+$/.test(msg.data))
6277
- return;
7143
+ // Mouse protocols can encode approvals/actions as well as wheel input.
7144
+ // A read-only view capability must never forward bytes to the backend.
7145
+ if (!authedClients.has(ws))
7146
+ return;
7147
+ if (usesHerdrSnapshotWebHistory()) {
7148
+ if (msg.data.includes('\x1b[<64;'))
7149
+ herdrWebScrollDirection = 'up';
7150
+ else if (msg.data.includes('\x1b[<65;'))
7151
+ herdrWebScrollDirection = 'down';
6278
7152
  }
6279
7153
  backend?.write(msg.data);
6280
7154
  }
@@ -6283,6 +7157,11 @@ function startWebServer(host, preferredPort) {
6283
7157
  });
6284
7158
  ws.on('close', () => {
6285
7159
  wsClients.delete(ws);
7160
+ herdrWebBindings.delete(ws);
7161
+ const promoted = herdrWebBinding.release();
7162
+ if (promoted?.readyState === WebSocket.OPEN) {
7163
+ promoted.send('\x1b]1989;owner\x07');
7164
+ }
6286
7165
  });
6287
7166
  }
6288
7167
  });
@@ -6295,7 +7174,7 @@ function startWebServer(host, preferredPort) {
6295
7174
  .then(resolve, reject);
6296
7175
  });
6297
7176
  }
6298
- function getTerminalHtml(hasWrite, platformReadonly = false, loginUrl = '') {
7177
+ function getTerminalHtml(hasWrite, platformReadonly = false, loginUrl = '', forceRemoteScroll = false) {
6299
7178
  const label = sessionId.substring(0, 8);
6300
7179
  return `<!DOCTYPE html>
6301
7180
  <html>
@@ -6307,8 +7186,8 @@ function getTerminalHtml(hasWrite, platformReadonly = false, loginUrl = '') {
6307
7186
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5/css/xterm.min.css">
6308
7187
  <style>
6309
7188
  *{margin:0;padding:0;box-sizing:border-box}
6310
- html,body{height:100%;background:#1a1b26;overflow:hidden;overscroll-behavior:none}
6311
- body{display:flex;flex-direction:column}
7189
+ html,body{width:100%;height:100%;background:#1a1b26;overflow:hidden;overscroll-behavior:none}
7190
+ body{display:flex;flex-direction:column;height:100vh;height:100dvh}
6312
7191
  #safe-area-probe{position:fixed;visibility:hidden;pointer-events:none;
6313
7192
  padding:env(safe-area-inset-top,0px) env(safe-area-inset-right,0px) env(safe-area-inset-bottom,0px) env(safe-area-inset-left,0px)}
6314
7193
  #toolbar-shell{--toolbar-scale:1;display:none;position:fixed;z-index:100;
@@ -6363,7 +7242,7 @@ body{display:flex;flex-direction:column}
6363
7242
  background:rgba(36,40,59,0.74)!important;touch-action:none}
6364
7243
  #toolbar.idle{opacity:.82}
6365
7244
  @media(prefers-reduced-motion:reduce){#toolbar,#toolbar.collapsed{transition:opacity .12s linear}}
6366
- #terminal{flex:1;min-height:0}
7245
+ #terminal{flex:1;min-width:0;min-height:0;width:100%;height:100%}
6367
7246
  #terminal .xterm{height:100%}
6368
7247
  /* Real scroll container is xterm's own viewport — kill iOS rubber-band bounce
6369
7248
  and momentum here (not just on body), and reserve gestures for pinch-zoom so
@@ -6428,9 +7307,10 @@ ${loginUrl ? `<a id="login-banner" href="${loginUrl}" target="_top" rel="noopene
6428
7307
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-canvas@0/lib/addon-canvas.min.js"></script>
6429
7308
  <script>
6430
7309
  var isTouch='ontouchstart'in window||navigator.maxTouchPoints>0;
6431
- if(isTouch){document.getElementById('vp').content='width=1100,viewport-fit=cover';document.body.classList.add('touch');}
7310
+ if(isTouch){document.body.classList.add('touch');}
6432
7311
  var hasToken=${hasWrite};
6433
7312
  var platformReadonly=${platformReadonly};
7313
+ var remoteScroll=${forceRemoteScroll};
6434
7314
  if(!hasToken){
6435
7315
  if(platformReadonly){var _lb=document.getElementById('login-banner');_lb.classList.add('show');}
6436
7316
  else{var _rb=document.getElementById('readonly-banner');_rb.classList.add('show');_rb.addEventListener('click',function(){_rb.classList.remove('show')});}
@@ -6536,9 +7416,9 @@ document.getElementById('terminal').addEventListener('contextmenu',function(e){e
6536
7416
  var ws_=null,el=document.getElementById('status');
6537
7417
  term.onData(function(d){
6538
7418
  if(!hasToken){
6539
- // Allow mouse events through (scroll/click) server accepts these in read-only.
6540
- // Keyboard input triggers the toast instead.
6541
- if(!/^\\x1b\\[[<M]/.test(d)){_showReadonlyToast();return;}
7419
+ // Mouse escape sequences are input too: a TUI can bind clicks or wheel
7420
+ // events to actions. View links never forward terminal bytes.
7421
+ _showReadonlyToast();return;
6542
7422
  }
6543
7423
  if(ws_&&ws_.readyState===1)ws_.send(JSON.stringify({type:'input',data:d}));
6544
7424
  });
@@ -6561,13 +7441,14 @@ function onViewportResize(){
6561
7441
  }
6562
7442
  window.addEventListener('resize',onViewportResize);
6563
7443
  (function connect(){
6564
- var t=new URLSearchParams(location.search).get('token')||'';
6565
7444
  // Derive base from the current path so the WS connects to the same prefix the
6566
7445
  // page was served under — works both directly (path '/') and behind the
6567
- // per-daemon reverse proxy ('/s/{sessionId}'). See terminal-proxy.ts.
7446
+ // per-daemon reverse proxy ('/s/{sessionId}'). Preserve the complete query:
7447
+ // write links carry token, while read-only links carry the distinct
7448
+ // viewToken capability. See terminal-proxy.ts.
6568
7449
  var base=location.pathname.replace(/\\/+$/,'');
6569
7450
  var proto=location.protocol==='https:'?'wss':'ws';
6570
- var ws=new WebSocket(proto+'://'+location.host+base+'/?token='+t);
7451
+ var ws=new WebSocket(proto+'://'+location.host+base+'/'+location.search);
6571
7452
  ws_=ws;ws.binaryType='arraybuffer';
6572
7453
  // Force a resize on every (re)connect: clear the dedup memory first. On
6573
7454
  // reconnect the browser grid is usually unchanged, so without this the
@@ -6579,6 +7460,36 @@ window.addEventListener('resize',onViewportResize);
6579
7460
  ws.onopen=function(){el.textContent='connected';el.className='ok';_lastC=_lastR=0;sendResize()};
6580
7461
  ws.onmessage=function(e){
6581
7462
  var data=typeof e.data==='string'?e.data:new TextDecoder().decode(e.data);
7463
+ // Snapshot-aware Herdr history replaces the buffer instead of appending a
7464
+ // mostly-overlapping full screen. Preserve the reader's anchor when older
7465
+ // rows were prepended; otherwise preserve their current position/follow.
7466
+ var _hh=data.match(/^\x1b\]1989;history;([0-9]+)\x07/);
7467
+ if(_hh){
7468
+ var _ha=+_hh[1],_hb=term.buffer.active,_hy=_hb.viewportY,_hBottom=_hy===_hb.baseY;
7469
+ data=data.slice(_hh[0].length);_cancelInitialFollow();term.reset();term.clear();
7470
+ data='\\x1b[2J\\x1b[H'+data;
7471
+ term.write(data,function(){
7472
+ if(_ha>0)term.scrollToLine(_hy+_ha);
7473
+ else if(_hBottom)term.scrollToBottom();
7474
+ else term.scrollToLine(_hy);
7475
+ });
7476
+ return;
7477
+ }
7478
+ // Managed Herdr has one authoritative pane grid shared by every viewer.
7479
+ // Followers render at the owner's grid; a promoted owner re-fits to its
7480
+ // own viewport and reports the new size back to the worker.
7481
+ var _hf=data.match(/\\x1b\\]1989;follower;(\\d+);(\\d+)\\x07/);
7482
+ if(_hf){
7483
+ fixedSize=true;var _hc=+_hf[1],_hr=+_hf[2];
7484
+ if(_hc>0&&_hr>0){try{term.resize(_hc,_hr)}catch(ex){}_lastC=_hc;_lastR=_hr}
7485
+ data=data.replace(_hf[0],'');
7486
+ }
7487
+ var _ho=data.match(/\\x1b\\]1989;owner\\x07/);
7488
+ if(_ho){
7489
+ fixedSize=false;data=data.replace(_ho[0],'');
7490
+ try{fit.fit()}catch(ex){}
7491
+ _lastC=_lastR=0;sendResize();
7492
+ }
6582
7493
  // botmux OSC 1989: pin the xterm to the adopted pane's fixed size (the pane
6583
7494
  // can't be resized, so FitAddon-to-browser would wrap the snapshot lines).
6584
7495
  var _fs=data.match(/\\x1b\\]1989;(\\d+);(\\d+)\\x07/);
@@ -6597,17 +7508,30 @@ window.addEventListener('resize',onViewportResize);
6597
7508
  // tmux — their whole transcript is redrawn by the app inside the fixed alt-screen
6598
7509
  // grid, so term.scrollLines() reveals nothing. In the alternate buffer we forward
6599
7510
  // scrolling as SGR mouse-wheel events so the CLI scrolls its own transcript and
6600
- // repaints (works in read-only too: the server only lets wheel sequences through).
7511
+ // repaints. This is write-capability only; view links stay locally scrollable.
6601
7512
  // Normal-buffer CLIs keep xterm's native scrollback scroll. Capture-phase +
6602
7513
  // stopPropagation pre-empts xterm's own handler. Skipped for pure tmux/zellij
6603
7514
  // ATTACH (gate), where the attach client owns scrolling via copy-mode.
6604
7515
  //
6605
- // Accumulate intended scroll DISTANCE (px) and emit one wheel tick per STEP px
6606
- // decoupled from how many wheel/touch events the browser fires per gesture
6607
- // (high-res trackpads fire dozens), so a small gesture stays a small scroll and
6608
- // doesn't compound into a whole screen. px<0 = scroll up (toward history). The
6609
- // per-call cap stops a single huge delta (page tick / fling) from over-firing.
6610
- var _scrollAccum=0;var _SCROLL_STEP=33;
7516
+ // Accumulate intended scroll DISTANCE (px) and emit one wheel tick per STEP px.
7517
+ // A high-resolution trackpad emits dozens of wheel events for one gesture, so
7518
+ // cap the whole continuous burst not each browser event then require an idle
7519
+ // gap (or direction reversal) before loading the next history chunk.
7520
+ var _scrollAccum=0,_scrollBurstTicks=0,_scrollBurstDir=0,_scrollBurstT=0;
7521
+ var _SCROLL_STEP=33;var _SCROLL_BURST_MAX=6;var _SCROLL_BURST_IDLE_MS=250;
7522
+ function _endScrollBurst(){
7523
+ clearTimeout(_scrollBurstT);_scrollBurstT=0;
7524
+ _scrollAccum=0;_scrollBurstTicks=0;_scrollBurstDir=0;
7525
+ }
7526
+ // Snapshot-backed remote TUIs can still accumulate useful local xterm history.
7527
+ // Consume it first; request another remote chunk only when the user pushes past
7528
+ // the local top/bottom boundary in that direction.
7529
+ function _canScrollLocal(px){
7530
+ var b=term.buffer.active;
7531
+ if(b.type==='alternate'||!px)return false;
7532
+ if(!remoteScroll)return true;
7533
+ return px>0||b.viewportY>0;
7534
+ }
6611
7535
  // Map a viewport pixel (clientX/Y) to a 1-based terminal cell "col;row", clamped to
6612
7536
  // the grid. The forwarded SGR wheel event MUST carry the cell UNDER THE POINTER, the
6613
7537
  // way a physical terminal reports it: zone-routed alt-screen TUIs — OpenCode (Bubble
@@ -6631,27 +7555,36 @@ function _cellAt(clientX,clientY){
6631
7555
  return col+';'+row;
6632
7556
  }
6633
7557
  function _fwdScroll(px,coord){
6634
- if(!ws_||ws_.readyState!==1)return;
7558
+ if(!hasToken||!ws_||ws_.readyState!==1||!px)return;
6635
7559
  coord=coord||(((term.cols>>1)+1)+';'+((term.rows>>1)+1)); // never (1,1)
7560
+ var dir=px<0?-1:1;
7561
+ if(_scrollBurstDir&&dir!==_scrollBurstDir){_scrollAccum=0;_scrollBurstTicks=0;}
7562
+ _scrollBurstDir=dir;
7563
+ clearTimeout(_scrollBurstT);_scrollBurstT=setTimeout(_endScrollBurst,_SCROLL_BURST_IDLE_MS);
7564
+ if(_scrollBurstTicks>=_SCROLL_BURST_MAX)return;
6636
7565
  _scrollAccum+=px;var data='',n=0;
6637
- while(Math.abs(_scrollAccum)>=_SCROLL_STEP&&n<6){
7566
+ while(Math.abs(_scrollAccum)>=_SCROLL_STEP&&n<6&&_scrollBurstTicks<_SCROLL_BURST_MAX){
6638
7567
  var up=_scrollAccum<0; // px<0 → wheel-up (history)
6639
7568
  data+='\\x1b[<'+(up?64:65)+';'+coord+'M';
6640
- _scrollAccum+=up?_SCROLL_STEP:-_SCROLL_STEP;n++;
7569
+ _scrollAccum+=up?_SCROLL_STEP:-_SCROLL_STEP;n++;_scrollBurstTicks++;
6641
7570
  }
7571
+ if(_scrollBurstTicks>=_SCROLL_BURST_MAX)_scrollAccum=0;
6642
7572
  if(data)ws_.send(JSON.stringify({type:'input',data:data}));
6643
7573
  }
6644
7574
  if(!${isTmuxMode && !isPipeMode}){
6645
7575
  document.getElementById('terminal').addEventListener('wheel',function(e){
6646
- if(term.buffer.active.type!=='alternate'){
7576
+ // Normalise deltaMode to px: line→~16px, page→~one screen.
7577
+ var px=e.deltaMode===1?e.deltaY*16:e.deltaMode===2?e.deltaY*term.rows*16:e.deltaY;
7578
+ if(_canScrollLocal(px)){
6647
7579
  // Normal buffer: xterm scrolls its own scrollback natively. In read-only a
6648
7580
  // mouse-mode CLI could swallow the wheel, so drive scrollback directly.
6649
- if(!hasToken){e.preventDefault();e.stopPropagation();term.scrollLines(e.deltaY>0?3:-3);}
7581
+ if(!hasToken){e.preventDefault();e.stopPropagation();term.scrollLines(px>0?3:-3);}
6650
7582
  return;
6651
7583
  }
7584
+ if(!hasToken){
7585
+ e.preventDefault();e.stopPropagation();term.scrollLines(e.deltaY>0?3:-3);return;
7586
+ }
6652
7587
  e.preventDefault();e.stopPropagation();
6653
- // Normalise deltaMode to px: line→~16px, page→~one screen.
6654
- var px=e.deltaMode===1?e.deltaY*16:e.deltaMode===2?e.deltaY*term.rows*16:e.deltaY;
6655
7588
  _fwdScroll(px,_cellAt(e.clientX,e.clientY)); // report the cell under the pointer
6656
7589
  },{capture:true,passive:false});
6657
7590
  }
@@ -6956,35 +7889,79 @@ if(isTouch&&hasToken){
6956
7889
  _scheduleToolbarLayout();_wakeToolbar();
6957
7890
  }
6958
7891
 
6959
- // Single-finger touch scrolling: normal-buffer CLIs use xterm's own Viewport
6960
- // (handleTouchMove scrollTop) natively. Alt-screen CLIs (Claude) have no xterm
6961
- // scrollback, so native touch scroll does nothing mirror the wheel fix and
6962
- // forward the drag to the CLI as SGR wheel events so it scrolls its own
6963
- // transcript. Only the alternate buffer is intercepted (capture + stopPropagation);
6964
- // the normal buffer falls through to xterm untouched, so no double-drive of
6965
- // scrollTop. overscroll-behavior:none (see <style>) kills the iOS rubber-band.
7892
+ // Single-finger touch scrolling: drive normal-buffer scrollback explicitly.
7893
+ // Some embedded WebViews do not perform xterm/browser native touch scrolling
7894
+ // when touch-action is restricted for pinch zoom. Handling in capture phase and
7895
+ // stopping propagation also prevents xterm from double-driving the viewport.
7896
+ // Alt-screen CLIs have no xterm scrollback, so forward the drag as SGR wheel
7897
+ // events and let the CLI scroll its own transcript.
6966
7898
  if(!${isTmuxMode && !isPipeMode}){
6967
7899
  var _tTerm=document.getElementById('terminal');
7900
+ var _tViewport=document.querySelector('#terminal .xterm-viewport');
6968
7901
  var _tLastY=null;
6969
7902
  _tTerm.addEventListener('touchstart',function(e){
6970
7903
  if(e.touches.length===1)_tLastY=e.touches[0].clientY;
6971
7904
  },{capture:true,passive:true});
6972
7905
  _tTerm.addEventListener('touchmove',function(e){
6973
- // Normal buffer / multi-touch / no start → let xterm (or the browser) handle it.
6974
- if(term.buffer.active.type!=='alternate'||_tLastY===null||e.touches.length!==1)return;
7906
+ // View links never forward input; let xterm/browser handle local scrolling.
7907
+ if(!hasToken||_tLastY===null||e.touches.length!==1)return;
6975
7908
  e.preventDefault();e.stopPropagation();
6976
7909
  var y=e.touches[0].clientY;
7910
+ var px=_tLastY-y;
7911
+ if(_canScrollLocal(px)){
7912
+ if(_tViewport)_tViewport.scrollTop-=y-_tLastY;
7913
+ else term.scrollLines(y>_tLastY?-1:1);
7914
+ _tLastY=y;return;
7915
+ }
6977
7916
  // finger drags down (y grows) → px<0 → scroll up (history); report the touched cell
6978
- _fwdScroll(_tLastY-y,_cellAt(e.touches[0].clientX,y));
7917
+ _fwdScroll(px,_cellAt(e.touches[0].clientX,y));
6979
7918
  _tLastY=y;
6980
7919
  },{capture:true,passive:false});
6981
- _tTerm.addEventListener('touchend',function(){_tLastY=null;},{capture:true,passive:true});
7920
+ _tTerm.addEventListener('touchend',function(){_tLastY=null;_endScrollBurst()},{capture:true,passive:true});
7921
+ _tTerm.addEventListener('touchcancel',function(){_tLastY=null;_endScrollBurst()},{capture:true,passive:true});
6982
7922
  }
6983
7923
  </script>
6984
7924
  </body>
6985
7925
  </html>`;
6986
7926
  }
6987
- // ─── IPC Communication ───────────────────────────────────────────────────────
7927
+ const emittedTurnTerminals = new TurnTerminalDeduper();
7928
+ /** Report CLI processing completion independently from user-visible output.
7929
+ * Keep a bounded worker-local dedup set because transcript watchers and app
7930
+ * runners may surface the same final boundary more than once. */
7931
+ function emitTurnTerminal(turnId, status, errorCode, dispatchAttempt) {
7932
+ if (!sessionId || !turnId)
7933
+ return;
7934
+ if (!emittedTurnTerminals.claim(sessionId, turnId, dispatchAttempt))
7935
+ return;
7936
+ if (status !== 'completed') {
7937
+ const dropped = codexBridgeQueue.dropPendingTurn(turnId, dispatchAttempt);
7938
+ if (dropped) {
7939
+ log(`Structured bridge retired terminal attempt turn=${turnId.slice(0, 12)} attempt=${dispatchAttempt ?? '-'} status=${status}`);
7940
+ }
7941
+ }
7942
+ // Revoke before publishing terminal. The daemon receives same-worker IPC in
7943
+ // order, and the worker-side relay becomes unusable synchronously.
7944
+ revokeManagedTurnOriginForTerminal(turnId, dispatchAttempt);
7945
+ send({
7946
+ type: 'turn_terminal',
7947
+ sessionId,
7948
+ turnId,
7949
+ status,
7950
+ ...(dispatchAttempt !== undefined ? { dispatchAttempt } : {}),
7951
+ ...(errorCode ? { errorCode } : {}),
7952
+ });
7953
+ if (terminalReleasesDurableTurn({ turnId: currentBotmuxTurnId, dispatchAttempt: currentBotmuxDispatchAttempt }, { turnId, dispatchAttempt })) {
7954
+ // Durable replay ownership moves back to the receiver/hub at terminal.
7955
+ // Do not let a later CLI exit carry this already-settled attempt into the
7956
+ // fresh process behind the receiver's newer dispatch attempt.
7957
+ inflightInputs.onTurnComplete();
7958
+ durableTurnInFlight = false;
7959
+ // The terminal may be emitted from inside an active flush continuation
7960
+ // (for example a synchronous hard-submit failure). Wake after that
7961
+ // continuation releases the mutex so queued work cannot remain stranded.
7962
+ queueMicrotask(() => { void flushPending(); });
7963
+ }
7964
+ }
6988
7965
  function workerIpcPayload(msg) {
6989
7966
  return msg.type === 'final_output' && sessionId
6990
7967
  ? { ...msg, sessionId }
@@ -7021,7 +7998,7 @@ let fatalWorkerErrorPending = false;
7021
7998
  * used both during init and after a previously-ready worker tries to recover
7022
7999
  * from a stopped/crashed CLI, so those later paths cannot regress to a silent
7023
8000
  * unhandled rejection/exception. */
7024
- async function sendFatalWorkerErrorAndExit(err, turnId = currentBotmuxTurnId) {
8001
+ async function sendFatalWorkerErrorAndExit(err, turnId = currentBotmuxTurnId, dispatchAttempt = currentBotmuxDispatchAttempt) {
7025
8002
  if (fatalWorkerErrorPending)
7026
8003
  return;
7027
8004
  fatalWorkerErrorPending = true;
@@ -7029,6 +8006,9 @@ async function sendFatalWorkerErrorAndExit(err, turnId = currentBotmuxTurnId) {
7029
8006
  type: 'error',
7030
8007
  message: err instanceof Error ? err.message : String(err),
7031
8008
  turnId,
8009
+ // Carry the durable attempt so the daemon can leave a meeting delivery
8010
+ // failure to the receipt/lease recovery path instead of replying out-of-band.
8011
+ ...(dispatchAttempt !== undefined ? { dispatchAttempt } : {}),
7032
8012
  });
7033
8013
  process.exit(1);
7034
8014
  }
@@ -7045,6 +8025,7 @@ process.on('message', async (raw) => {
7045
8025
  return; // already initialized
7046
8026
  lastInitConfig = msg;
7047
8027
  sessionId = msg.sessionId;
8028
+ refreshTerminalViewToken();
7048
8029
  if (msg.ownerOpenId)
7049
8030
  process.env.__OWNER_OPEN_ID = msg.ownerOpenId;
7050
8031
  // Pin this worker's i18n locale early so every t() call below resolves
@@ -7078,7 +8059,10 @@ process.on('message', async (raw) => {
7078
8059
  try {
7079
8060
  if (msg.turnId) {
7080
8061
  currentBotmuxTurnId = msg.turnId;
8062
+ currentBotmuxDispatchAttempt = msg.dispatchAttempt;
8063
+ currentVcMeetingImTurnOrigin = msg.vcMeetingImTurnOrigin;
7081
8064
  writeCliPidMarker();
8065
+ publishSandboxRelayCapability();
7082
8066
  }
7083
8067
  let port = 0;
7084
8068
  if (!isWorkflowWorker()) {
@@ -7111,6 +8095,10 @@ process.on('message', async (raw) => {
7111
8095
  hasStartupCommands: !!msg.startupCommands?.length,
7112
8096
  adoptMode: msg.adoptMode === true,
7113
8097
  passesInitialPromptViaArgs: cliAdapter?.passesInitialPromptViaArgs === true,
8098
+ }) || shouldDeferArgsBakedDurablePrompt({
8099
+ passesInitialPromptViaArgs: cliAdapter?.passesInitialPromptViaArgs === true,
8100
+ adoptMode: msg.adoptMode === true,
8101
+ dispatchAttempt: msg.dispatchAttempt,
7114
8102
  }) || (msg.adoptMode !== true && lastSpawnEffectiveResume && cliAdapter?.initialPromptArgsIgnoredOnResume === true);
7115
8103
  if (msg.prompt && cliAdapter?.passesInitialPromptViaArgs && !deferInitialPrompt && codexBridgeFallbackActive()) {
7116
8104
  // Args-baked first prompts (notably Pi) never pass through the normal
@@ -7119,12 +8107,14 @@ process.on('message', async (raw) => {
7119
8107
  // Mark it here before the CLI starts processing; late-attach is fine
7120
8108
  // because CodexBridgeQueue is path-agnostic until ingest discovers the
7121
8109
  // transcript file.
7122
- codexBridgeMarkPendingTurn(msg.prompt, msg.turnId);
8110
+ codexBridgeMarkPendingTurn(msg.prompt, msg.turnId, msg.dispatchAttempt);
7123
8111
  }
7124
8112
  if (msg.prompt && (!cliAdapter?.passesInitialPromptViaArgs || deferInitialPrompt)) {
7125
8113
  pendingMessages.push({
7126
8114
  content: msg.prompt,
7127
8115
  turnId: msg.turnId,
8116
+ dispatchAttempt: msg.dispatchAttempt,
8117
+ vcMeetingImTurnOrigin: msg.vcMeetingImTurnOrigin,
7128
8118
  codexAppInput: msg.promptCodexAppInput,
7129
8119
  });
7130
8120
  }
@@ -7136,7 +8126,14 @@ process.on('message', async (raw) => {
7136
8126
  if (effectiveBackendType === 'riff' && pendingMessages.length > 0) {
7137
8127
  flushPending();
7138
8128
  }
7139
- send({ type: 'ready', port, token: writeToken, turnId: currentBotmuxTurnId });
8129
+ send({
8130
+ type: 'ready',
8131
+ port,
8132
+ token: writeToken,
8133
+ viewToken,
8134
+ turnId: currentBotmuxTurnId,
8135
+ dispatchAttempt: currentBotmuxDispatchAttempt,
8136
+ });
7140
8137
  }
7141
8138
  catch (err) {
7142
8139
  await sendFatalWorkerErrorAndExit(err);
@@ -7161,8 +8158,6 @@ process.on('message', async (raw) => {
7161
8158
  deferredPluginSkillCatalog = null;
7162
8159
  log('Attached refreshed plugin Skill catalog to the first turn of this CLI generation');
7163
8160
  }
7164
- currentBotmuxTurnId = msg.turnId;
7165
- writeCliPidMarker();
7166
8161
  if (!backend && crashDiagnosticStopped && lastInitConfig && !lastInitConfig.adoptMode) {
7167
8162
  log('Message received after crash-loop stop; retrying CLI start');
7168
8163
  destroyCrashDiagnosticTerminal('retry after message');
@@ -7175,11 +8170,23 @@ process.on('message', async (raw) => {
7175
8170
  spawnCli({ ...lastInitConfig, resume: true, prompt: '' });
7176
8171
  }
7177
8172
  catch (err) {
7178
- await sendFatalWorkerErrorAndExit(err, msg.turnId);
8173
+ // Pass the message's own attempt (not the stale currentBotmux* from a
8174
+ // prior IM turn) so a durable delivery relaunch failure carries the
8175
+ // right attribution for the daemon's receipt/lease gate.
8176
+ await sendFatalWorkerErrorAndExit(err, msg.turnId, msg.dispatchAttempt);
7179
8177
  return;
7180
8178
  }
7181
8179
  }
7182
8180
  if (lastInitConfig?.adoptMode) {
8181
+ // Adopt writes immediately below, so this turn really becomes current
8182
+ // now. Non-adopt messages update the marker only when flushPending
8183
+ // dequeues/writes them; doing it at IPC arrival lets a queued IM turn
8184
+ // steal the identity of a still-running durable delivery.
8185
+ currentBotmuxTurnId = msg.turnId;
8186
+ currentBotmuxDispatchAttempt = msg.dispatchAttempt;
8187
+ currentVcMeetingImTurnOrigin = msg.vcMeetingImTurnOrigin;
8188
+ writeCliPidMarker();
8189
+ publishSandboxRelayCapability();
7183
8190
  // Bridge mode: capture transcript baseline BEFORE writing to the pane,
7184
8191
  // so any assistant uuids appended after this point are attributed to
7185
8192
  // *this* Lark turn (not local user activity in the pane). Mark may
@@ -7190,7 +8197,7 @@ process.on('message', async (raw) => {
7190
8197
  bridgeIngest();
7191
8198
  }
7192
8199
  catch { /* best effort */ }
7193
- bridgeMarkPendingTurn(content, msg.turnId);
8200
+ bridgeMarkPendingTurn(content, msg.turnId, msg.dispatchAttempt);
7194
8201
  }
7195
8202
  else if (codexBridgeFallbackActive()) {
7196
8203
  // Codex adopt: same idea, different bridge. ingest first so any
@@ -7202,7 +8209,7 @@ process.on('message', async (raw) => {
7202
8209
  // before this IPC message is handled. Mark first so that preexisting
7203
8210
  // current-line can still fingerprint-match instead of being marked
7204
8211
  // seen as an unmatched event.
7205
- codexBridgeMarkPendingTurn(content, msg.turnId);
8212
+ codexBridgeMarkPendingTurn(content, msg.turnId, msg.dispatchAttempt);
7206
8213
  try {
7207
8214
  codexBridgeIngest();
7208
8215
  }
@@ -7213,7 +8220,7 @@ process.on('message', async (raw) => {
7213
8220
  codexBridgeIngest();
7214
8221
  }
7215
8222
  catch { /* best effort */ }
7216
- codexBridgeMarkPendingTurn(content, msg.turnId);
8223
+ codexBridgeMarkPendingTurn(content, msg.turnId, msg.dispatchAttempt);
7217
8224
  }
7218
8225
  }
7219
8226
  // Adopt mode write:
@@ -7273,17 +8280,28 @@ process.on('message', async (raw) => {
7273
8280
  // arrival. Marking now would race with a still-running previous
7274
8281
  // turn whose `botmux send` could sneak its sentAtMs past this
7275
8282
  // turn's markTimeMs and falsely suppress its fallback.
7276
- sendToPty(content, msg.turnId, codexAppInput);
8283
+ sendToPty(content, msg.turnId, {
8284
+ codexAppInput,
8285
+ dispatchAttempt: msg.dispatchAttempt,
8286
+ vcMeetingImTurnOrigin: msg.vcMeetingImTurnOrigin,
8287
+ });
7277
8288
  }
7278
8289
  break;
7279
8290
  }
7280
8291
  case 'raw_input': {
7281
8292
  // Preserve legacy busy delivery (/btw and other steering commands). A
7282
- // native /rename already submitted to the TUI is the sole exception: its
7283
- // command UI must return to the prompt before another slash command lands.
7284
- if (sessionRenameInFlight) {
8293
+ // native /rename and an owned CLI restart are the exceptions: never splice
8294
+ // into the rename UI, write through an old backend during async teardown,
8295
+ // or type into the replacement before its real prompt is ready.
8296
+ // TUI 注入是第三个例外:注入进行中(injectionFlushing——Serially 只互斥
8297
+ // text→Enter 短窗口,覆盖不了注入后的 quiescence 等待)或队列里有 cwd
8298
+ // barrier(shouldDeferUserFlush——/cd 未落地前任何用户输入都不得写入,
8299
+ // 否则 passthrough 会执行在旧 cwd 的 CLI 里)时入队。注入排空后由
8300
+ // flushPendingInjections 的 finally 补踢 flushPending 送达。
8301
+ if (cliRestartInProgress || rawInputRestartGate || sessionRenameInFlight
8302
+ || injectionFlushing || shouldDeferUserFlush(pendingInjections)) {
7285
8303
  pendingRawInputs.push(msg);
7286
- log(`Deferred passthrough slash command until native rename settles: ${msg.content}`);
8304
+ log(`Deferred passthrough slash command until CLI input gate settles: ${msg.content}`);
7287
8305
  }
7288
8306
  else {
7289
8307
  await deliverRawInput(msg);
@@ -7310,71 +8328,99 @@ process.on('message', async (raw) => {
7310
8328
  break;
7311
8329
  }
7312
8330
  case 'restart': {
7313
- if (lastInitConfig?.adoptMode) {
7314
- log('Restart ignored in adopt mode');
8331
+ await restartCliProcess('daemon request', { preservePending: true });
8332
+ break;
8333
+ }
8334
+ case 'expire_durable_turn': {
8335
+ const acknowledge = (disposition) => send({
8336
+ type: 'durable_expiry_ready',
8337
+ sessionId,
8338
+ turnId: msg.turnId,
8339
+ dispatchAttempt: msg.dispatchAttempt,
8340
+ disposition,
8341
+ });
8342
+ const currentExact = durableTurnInFlight
8343
+ && currentBotmuxTurnId === msg.turnId
8344
+ && currentBotmuxDispatchAttempt === msg.dispatchAttempt;
8345
+ if (currentExact) {
8346
+ if (lastInitConfig?.adoptMode) {
8347
+ log('Refused durable expiry ACK for adopt-mode session');
8348
+ break;
8349
+ }
8350
+ log(`Expiring active durable turn=${msg.turnId.slice(0, 12)} attempt=${msg.dispatchAttempt}; `
8351
+ + 'restarting CLI with queued follow-ups preserved');
8352
+ durableTurnInFlight = false;
8353
+ inflightInputs.onTurnComplete();
8354
+ // immediate=true bypasses tmux jitter. Await teardown (including Riff's
8355
+ // async remote cancellation) before ACKing so replay cannot overlap the
8356
+ // old owned CLI; the replacement spawn itself remains delayed by 500ms.
8357
+ await restartCliProcess('durable lease expiry', { immediate: true, preservePending: true });
8358
+ acknowledge('cli_fenced');
7315
8359
  break;
7316
8360
  }
7317
- log('Restart requested');
7318
- // Tier-2 guard: 2nd consecutive in-worker restart forces FRESH.
7319
- // Increment BEFORE spawnCli so the guard trips at count==2 (i.e. the
7320
- // third attempted spawn in a 1-success 2-failure sequence):
7321
- // initial spawn (count=0) fail claude_exit daemon sends restart
7322
- // 1st restart (count=1) → resume still fails → restart
7323
- // 2nd restart (count=2) tier-2 kicks in FRESH
7324
- // Tier 1 probe (adapter.checkResumeTargetExists) is re-run on each
7325
- // spawn, so even count=1 often short-circuits; tier-2 only catches
7326
- // silent/race failures and adapters that don't implement the probe.
7327
- consecutiveInWorkerRestarts++;
7328
- log(`Restart count: ${consecutiveInWorkerRestarts} (>=2 forces FRESH)`);
7329
- // Must destroySession(), not kill(): for persistent backends (tmux/herdr)
7330
- // kill() only detaches — the backing session + CLI process keep running,
7331
- // so the resume:true spawnCli below would re-attach to the SAME live CLI
7332
- // (selectSessionBackend reattaches whenever hasSession() is true) and the
7333
- // process would never actually restart. destroySession() tears the session
7334
- // down so the respawn starts a fresh CLI. (PTY has no destroySession, so
7335
- // the ?. no-ops and killCli()'s kill() does the teardown.)
7336
- const restart = async () => {
7337
- tmuxRestartTimer = null;
7338
- // riff:teardown=远端 task-cancel(异步)。必须等它完成再 spawn,否则
7339
- // 旧任务与新任务并跑、双方都往话题里发消息。
7340
- const t = backend?.destroySession?.();
7341
- if (t && typeof t.then === 'function') {
7342
- try {
7343
- await Promise.race([t, new Promise((r) => setTimeout(r, 22_000))]);
7344
- }
7345
- catch { /* logged inside destroySession */ }
8361
+ // A receipt's lease begins at daemon dispatch claim, before worker IPC
8362
+ // necessarily reaches the PTY. It can therefore expire behind a long
8363
+ // ordinary IM turn. Remove that exact queued generation and ACK without
8364
+ // restarting the unrelated active turn; otherwise attempt N survives in
8365
+ // the queue and executes after the hub has already replayed N+1.
8366
+ let removedPending = 0;
8367
+ for (let i = pendingMessages.length - 1; i >= 0; i--) {
8368
+ const item = pendingMessages[i];
8369
+ if (item.turnId === msg.turnId && item.dispatchAttempt === msg.dispatchAttempt) {
8370
+ pendingMessages.splice(i, 1);
8371
+ removedPending++;
7346
8372
  }
7347
- killCli();
7348
- awaitingFirstPrompt = true;
7349
- setTimeout(async () => {
7350
- if (lastInitConfig) {
7351
- startScreenUpdates();
7352
- startScreenAnalyzer();
7353
- try {
7354
- spawnCli({ ...lastInitConfig, resume: true, prompt: '' });
7355
- }
7356
- catch (err) {
7357
- await sendFatalWorkerErrorAndExit(err);
7358
- }
7359
- }
7360
- }, 500);
7361
- };
7362
- if (effectiveBackendType === 'tmux') {
7363
- const delayMs = tmuxRestartJitterMs(lastInitConfig?.sessionId ?? '', consecutiveInWorkerRestarts);
7364
- log(`Staggering tmux teardown/restart by ${delayMs}ms to avoid shared-server probe storms`);
7365
- if (tmuxRestartTimer)
7366
- clearTimeout(tmuxRestartTimer);
7367
- tmuxRestartTimer = setTimeout(restart, delayMs);
7368
8373
  }
7369
- else {
7370
- restart();
8374
+ if (removedPending > 0) {
8375
+ log(`Expired ${removedPending} queued durable input(s) turn=${msg.turnId.slice(0, 12)} `
8376
+ + `attempt=${msg.dispatchAttempt}`);
8377
+ acknowledge('queued_removed');
8378
+ break;
7371
8379
  }
8380
+ // Neither active nor queued means this worker cannot prove where the
8381
+ // attempt went. Deliberately withhold ACK so the daemon's short timeout
8382
+ // escalates to an owned-pane teardown before replay is admitted.
8383
+ log(`Cannot prove durable expiry turn=${msg.turnId.slice(0, 12)} `
8384
+ + `attempt=${msg.dispatchAttempt}; withholding ACK for daemon fencing`);
8385
+ break;
8386
+ }
8387
+ case 'reset_ambiguous_receiver': {
8388
+ const receiver = sessionId ? sessionStore.getSession(sessionId)?.vcMeetingReceiver : undefined;
8389
+ if (!receiver) {
8390
+ log('Ignored boot recovery reset for a non-receiver session');
8391
+ break;
8392
+ }
8393
+ if (lastInitConfig?.adoptMode) {
8394
+ log('Refused boot recovery ACK for adopt-mode receiver identity');
8395
+ break;
8396
+ }
8397
+ log(`Boot recovery fencing ambiguous receiver turn=${msg.turnId.slice(0, 12)} `
8398
+ + `attempt=${msg.dispatchAttempt}`);
8399
+ durableTurnInFlight = false;
8400
+ inflightInputs.onTurnComplete();
8401
+ await restartCliProcess('ambiguous receiver boot recovery', { immediate: true, preservePending: true });
8402
+ send({
8403
+ type: 'receiver_reset_ready',
8404
+ sessionId: sessionId,
8405
+ turnId: msg.turnId,
8406
+ dispatchAttempt: msg.dispatchAttempt,
8407
+ });
7372
8408
  break;
7373
8409
  }
7374
8410
  case 'tui_keys': {
7375
8411
  handleTuiKeys(msg.keys, msg.isFinal);
7376
8412
  break;
7377
8413
  }
8414
+ case 'inject_command': {
8415
+ // 会话内 /cd 移动后,worker 内部 respawn(claude_exit 自动重启 / IM /restart /
8416
+ // dashboard restart)必须收敛到新目录,而不是陈旧的 lastInitConfig.workingDir。
8417
+ if (msg.updateWorkingDir && lastInitConfig) {
8418
+ lastInitConfig.workingDir = msg.updateWorkingDir;
8419
+ }
8420
+ pendingInjections.push({ command: msg.command, barrier: !!msg.updateWorkingDir });
8421
+ void flushPendingInjections();
8422
+ break;
8423
+ }
7378
8424
  case 'tui_text_input': {
7379
8425
  handleTuiTextInput(msg.keys, msg.text);
7380
8426
  break;
@@ -7473,6 +8519,7 @@ process.on('message', async (raw) => {
7473
8519
  // forkWorker(resume=true) → a fresh `new-session --resume <cliSessionId>`
7474
8520
  // that rebuilds context from the on-disk transcript (same path the daemon
7475
8521
  // uses to recover sessions after a reboot kills the tmux server).
8522
+ revokeManagedTurnOriginForRestart();
7476
8523
  try {
7477
8524
  // riff:suspend 语义是「休眠待续」——绝不能 cancel 远端任务(血缘已持久化,
7478
8525
  // 恢复时 follow-up 续上);只断流 detach。
@@ -7521,6 +8568,7 @@ function cleanup() {
7521
8568
  for (const ws of wsClients)
7522
8569
  ws.close();
7523
8570
  wsClients.clear();
8571
+ herdrWebBindings.clear();
7524
8572
  if (wss) {
7525
8573
  wss.close();
7526
8574
  wss = null;
@@ -7593,6 +8641,8 @@ function teardownSandboxBestEffort() {
7593
8641
  }
7594
8642
  catch { /* */ }
7595
8643
  sandboxCleanup = null;
8644
+ unlinkManagedOriginCapabilityFiles();
8645
+ sandboxRelayCapability = null;
7596
8646
  if (seatbeltProfilePath) {
7597
8647
  try {
7598
8648
  unlinkSync(seatbeltProfilePath);