botmux 3.16.0 → 3.17.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 (619) hide show
  1. package/dist/.runtime-build-id +1 -1
  2. package/dist/adapters/backend/riff-backend.d.ts +84 -1
  3. package/dist/adapters/backend/riff-backend.d.ts.map +1 -1
  4. package/dist/adapters/backend/riff-backend.js +202 -19
  5. package/dist/adapters/backend/riff-backend.js.map +1 -1
  6. package/dist/adapters/backend/sandbox.d.ts +8 -0
  7. package/dist/adapters/backend/sandbox.d.ts.map +1 -1
  8. package/dist/adapters/backend/sandbox.js +15 -0
  9. package/dist/adapters/backend/sandbox.js.map +1 -1
  10. package/dist/adapters/backend/tmux-backend.d.ts +61 -7
  11. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  12. package/dist/adapters/backend/tmux-backend.js +80 -9
  13. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  14. package/dist/adapters/backend/tmux-pipe-backend.d.ts +62 -4
  15. package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
  16. package/dist/adapters/backend/tmux-pipe-backend.js +240 -27
  17. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
  18. package/dist/adapters/cli/claude-code.d.ts.map +1 -1
  19. package/dist/adapters/cli/claude-code.js +4 -2
  20. package/dist/adapters/cli/claude-code.js.map +1 -1
  21. package/dist/adapters/cli/coco.d.ts.map +1 -1
  22. package/dist/adapters/cli/coco.js +27 -0
  23. package/dist/adapters/cli/coco.js.map +1 -1
  24. package/dist/adapters/cli/codex-app.d.ts.map +1 -1
  25. package/dist/adapters/cli/codex-app.js +28 -0
  26. package/dist/adapters/cli/codex-app.js.map +1 -1
  27. package/dist/adapters/cli/codex.d.ts.map +1 -1
  28. package/dist/adapters/cli/codex.js +51 -23
  29. package/dist/adapters/cli/codex.js.map +1 -1
  30. package/dist/adapters/cli/dsh-tui.d.ts +24 -0
  31. package/dist/adapters/cli/dsh-tui.d.ts.map +1 -0
  32. package/dist/adapters/cli/dsh-tui.js +99 -0
  33. package/dist/adapters/cli/dsh-tui.js.map +1 -0
  34. package/dist/adapters/cli/dsh.d.ts.map +1 -1
  35. package/dist/adapters/cli/dsh.js +15 -9
  36. package/dist/adapters/cli/dsh.js.map +1 -1
  37. package/dist/adapters/cli/fs-policy.d.ts +98 -1
  38. package/dist/adapters/cli/fs-policy.d.ts.map +1 -1
  39. package/dist/adapters/cli/fs-policy.js +182 -7
  40. package/dist/adapters/cli/fs-policy.js.map +1 -1
  41. package/dist/adapters/cli/grok.js +16 -18
  42. package/dist/adapters/cli/grok.js.map +1 -1
  43. package/dist/adapters/cli/model-catalog-json.d.ts +17 -0
  44. package/dist/adapters/cli/model-catalog-json.d.ts.map +1 -0
  45. package/dist/adapters/cli/model-catalog-json.js +43 -0
  46. package/dist/adapters/cli/model-catalog-json.js.map +1 -0
  47. package/dist/adapters/cli/oh-my-pi.d.ts +2 -0
  48. package/dist/adapters/cli/oh-my-pi.d.ts.map +1 -1
  49. package/dist/adapters/cli/oh-my-pi.js +45 -13
  50. package/dist/adapters/cli/oh-my-pi.js.map +1 -1
  51. package/dist/adapters/cli/opencode.js +1 -1
  52. package/dist/adapters/cli/opencode.js.map +1 -1
  53. package/dist/adapters/cli/registry.d.ts +2 -1
  54. package/dist/adapters/cli/registry.d.ts.map +1 -1
  55. package/dist/adapters/cli/registry.js +6 -1
  56. package/dist/adapters/cli/registry.js.map +1 -1
  57. package/dist/adapters/cli/relay.d.ts.map +1 -1
  58. package/dist/adapters/cli/relay.js +7 -0
  59. package/dist/adapters/cli/relay.js.map +1 -1
  60. package/dist/adapters/cli/seed.d.ts.map +1 -1
  61. package/dist/adapters/cli/seed.js +6 -0
  62. package/dist/adapters/cli/seed.js.map +1 -1
  63. package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
  64. package/dist/adapters/cli/shared-hints.js +20 -14
  65. package/dist/adapters/cli/shared-hints.js.map +1 -1
  66. package/dist/adapters/cli/traex.d.ts +4 -0
  67. package/dist/adapters/cli/traex.d.ts.map +1 -1
  68. package/dist/adapters/cli/traex.js +153 -0
  69. package/dist/adapters/cli/traex.js.map +1 -1
  70. package/dist/adapters/cli/types.d.ts +42 -1
  71. package/dist/adapters/cli/types.d.ts.map +1 -1
  72. package/dist/autostart.d.ts +6 -0
  73. package/dist/autostart.d.ts.map +1 -1
  74. package/dist/autostart.js +21 -1
  75. package/dist/autostart.js.map +1 -1
  76. package/dist/bot-registry.d.ts +27 -0
  77. package/dist/bot-registry.d.ts.map +1 -1
  78. package/dist/bot-registry.js +62 -2
  79. package/dist/bot-registry.js.map +1 -1
  80. package/dist/cli/vc-agent.d.ts.map +1 -1
  81. package/dist/cli/vc-agent.js +21 -3
  82. package/dist/cli/vc-agent.js.map +1 -1
  83. package/dist/cli.d.ts.map +1 -1
  84. package/dist/cli.js +92 -1
  85. package/dist/cli.js.map +1 -1
  86. package/dist/codex-app-runner.js +28 -0
  87. package/dist/codex-app-runner.js.map +1 -1
  88. package/dist/core/bot-steer-directive.d.ts +25 -0
  89. package/dist/core/bot-steer-directive.d.ts.map +1 -0
  90. package/dist/core/bot-steer-directive.js +47 -0
  91. package/dist/core/bot-steer-directive.js.map +1 -0
  92. package/dist/core/command-handler.d.ts.map +1 -1
  93. package/dist/core/command-handler.js +102 -18
  94. package/dist/core/command-handler.js.map +1 -1
  95. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  96. package/dist/core/dashboard-ipc-server.js +53 -13
  97. package/dist/core/dashboard-ipc-server.js.map +1 -1
  98. package/dist/core/dashboard-rows.d.ts +3 -0
  99. package/dist/core/dashboard-rows.d.ts.map +1 -1
  100. package/dist/core/dashboard-rows.js +26 -5
  101. package/dist/core/dashboard-rows.js.map +1 -1
  102. package/dist/core/dashboard-url.d.ts +10 -1
  103. package/dist/core/dashboard-url.d.ts.map +1 -1
  104. package/dist/core/dashboard-url.js +12 -2
  105. package/dist/core/dashboard-url.js.map +1 -1
  106. package/dist/core/dispatch.d.ts.map +1 -1
  107. package/dist/core/dispatch.js +5 -4
  108. package/dist/core/dispatch.js.map +1 -1
  109. package/dist/core/existing-app-server.d.ts +22 -0
  110. package/dist/core/existing-app-server.d.ts.map +1 -0
  111. package/dist/core/existing-app-server.js +85 -0
  112. package/dist/core/existing-app-server.js.map +1 -0
  113. package/dist/core/native-topic-id.d.ts +9 -0
  114. package/dist/core/native-topic-id.d.ts.map +1 -0
  115. package/dist/core/native-topic-id.js +14 -0
  116. package/dist/core/native-topic-id.js.map +1 -0
  117. package/dist/core/per-bot-env.d.ts.map +1 -1
  118. package/dist/core/per-bot-env.js +10 -0
  119. package/dist/core/per-bot-env.js.map +1 -1
  120. package/dist/core/plugins/mcp/gateway.d.ts +27 -3
  121. package/dist/core/plugins/mcp/gateway.d.ts.map +1 -1
  122. package/dist/core/plugins/mcp/gateway.js +272 -26
  123. package/dist/core/plugins/mcp/gateway.js.map +1 -1
  124. package/dist/core/plugins/mcp/host.d.ts +18 -0
  125. package/dist/core/plugins/mcp/host.d.ts.map +1 -1
  126. package/dist/core/plugins/mcp/host.js +80 -16
  127. package/dist/core/plugins/mcp/host.js.map +1 -1
  128. package/dist/core/plugins/mcp/launch-record.d.ts +14 -0
  129. package/dist/core/plugins/mcp/launch-record.d.ts.map +1 -0
  130. package/dist/core/plugins/mcp/launch-record.js +63 -0
  131. package/dist/core/plugins/mcp/launch-record.js.map +1 -0
  132. package/dist/core/session-activity.d.ts +11 -0
  133. package/dist/core/session-activity.d.ts.map +1 -1
  134. package/dist/core/session-activity.js +23 -0
  135. package/dist/core/session-activity.js.map +1 -1
  136. package/dist/core/session-group-birth.d.ts.map +1 -1
  137. package/dist/core/session-group-birth.js +23 -8
  138. package/dist/core/session-group-birth.js.map +1 -1
  139. package/dist/core/session-manager.d.ts +1 -4
  140. package/dist/core/session-manager.d.ts.map +1 -1
  141. package/dist/core/session-manager.js +16 -20
  142. package/dist/core/session-manager.js.map +1 -1
  143. package/dist/core/shared-adopt.d.ts +20 -0
  144. package/dist/core/shared-adopt.d.ts.map +1 -0
  145. package/dist/core/shared-adopt.js +26 -0
  146. package/dist/core/shared-adopt.js.map +1 -0
  147. package/dist/core/skills/customize-command.d.ts +7 -0
  148. package/dist/core/skills/customize-command.d.ts.map +1 -0
  149. package/dist/core/skills/customize-command.js +303 -0
  150. package/dist/core/skills/customize-command.js.map +1 -0
  151. package/dist/core/terminal-write-auth.d.ts +15 -0
  152. package/dist/core/terminal-write-auth.d.ts.map +1 -1
  153. package/dist/core/terminal-write-auth.js +15 -0
  154. package/dist/core/terminal-write-auth.js.map +1 -1
  155. package/dist/core/terminal-write-frame.d.ts +30 -0
  156. package/dist/core/terminal-write-frame.d.ts.map +1 -0
  157. package/dist/core/terminal-write-frame.js +44 -0
  158. package/dist/core/terminal-write-frame.js.map +1 -0
  159. package/dist/core/types.d.ts +14 -4
  160. package/dist/core/types.d.ts.map +1 -1
  161. package/dist/core/types.js +6 -8
  162. package/dist/core/types.js.map +1 -1
  163. package/dist/core/worker-pool.d.ts +31 -6
  164. package/dist/core/worker-pool.d.ts.map +1 -1
  165. package/dist/core/worker-pool.js +444 -102
  166. package/dist/core/worker-pool.js.map +1 -1
  167. package/dist/core/worker-startup-retry.d.ts +52 -0
  168. package/dist/core/worker-startup-retry.d.ts.map +1 -0
  169. package/dist/core/worker-startup-retry.js +68 -0
  170. package/dist/core/worker-startup-retry.js.map +1 -0
  171. package/dist/daemon.d.ts +9 -8
  172. package/dist/daemon.d.ts.map +1 -1
  173. package/dist/daemon.js +467 -171
  174. package/dist/daemon.js.map +1 -1
  175. package/dist/dashboard/autostart-api.d.ts +26 -0
  176. package/dist/dashboard/autostart-api.d.ts.map +1 -0
  177. package/dist/dashboard/autostart-api.js +88 -0
  178. package/dist/dashboard/autostart-api.js.map +1 -0
  179. package/dist/dashboard/bot-payload.d.ts +5 -0
  180. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  181. package/dist/dashboard/bot-payload.js +2 -0
  182. package/dist/dashboard/bot-payload.js.map +1 -1
  183. package/dist/dashboard/control-csrf.d.ts +29 -1
  184. package/dist/dashboard/control-csrf.d.ts.map +1 -1
  185. package/dist/dashboard/control-csrf.js +49 -10
  186. package/dist/dashboard/control-csrf.js.map +1 -1
  187. package/dist/dashboard/customization-api.d.ts +23 -0
  188. package/dist/dashboard/customization-api.d.ts.map +1 -0
  189. package/dist/dashboard/customization-api.js +198 -0
  190. package/dist/dashboard/customization-api.js.map +1 -0
  191. package/dist/dashboard/debug-terminal.d.ts +13 -1
  192. package/dist/dashboard/debug-terminal.d.ts.map +1 -1
  193. package/dist/dashboard/debug-terminal.js +18 -4
  194. package/dist/dashboard/debug-terminal.js.map +1 -1
  195. package/dist/dashboard/settings-write-applier.d.ts +0 -23
  196. package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
  197. package/dist/dashboard/settings-write-applier.js +7 -28
  198. package/dist/dashboard/settings-write-applier.js.map +1 -1
  199. package/dist/dashboard/terminal-control-route.d.ts +32 -0
  200. package/dist/dashboard/terminal-control-route.d.ts.map +1 -0
  201. package/dist/dashboard/terminal-control-route.js +70 -0
  202. package/dist/dashboard/terminal-control-route.js.map +1 -0
  203. package/dist/dashboard/terminal-control.d.ts +64 -13
  204. package/dist/dashboard/terminal-control.d.ts.map +1 -1
  205. package/dist/dashboard/terminal-control.js +77 -14
  206. package/dist/dashboard/terminal-control.js.map +1 -1
  207. package/dist/dashboard/terminal-front-proxy.d.ts.map +1 -1
  208. package/dist/dashboard/terminal-front-proxy.js +41 -9
  209. package/dist/dashboard/terminal-front-proxy.js.map +1 -1
  210. package/dist/dashboard/vc-consumer-profiles-api.d.ts +69 -18
  211. package/dist/dashboard/vc-consumer-profiles-api.d.ts.map +1 -1
  212. package/dist/dashboard/vc-consumer-profiles-api.js +149 -29
  213. package/dist/dashboard/vc-consumer-profiles-api.js.map +1 -1
  214. package/dist/dashboard/web/agent-workbench-api.d.ts +34 -2
  215. package/dist/dashboard/web/agent-workbench-api.d.ts.map +1 -1
  216. package/dist/dashboard/web/agent-workbench-api.js +39 -4
  217. package/dist/dashboard/web/agent-workbench-api.js.map +1 -1
  218. package/dist/dashboard/web/agent-workbench-appearance.d.ts +6 -3
  219. package/dist/dashboard/web/agent-workbench-appearance.d.ts.map +1 -1
  220. package/dist/dashboard/web/agent-workbench-appearance.js +7 -4
  221. package/dist/dashboard/web/agent-workbench-appearance.js.map +1 -1
  222. package/dist/dashboard/web/agent-workbench-model.d.ts +2 -0
  223. package/dist/dashboard/web/agent-workbench-model.d.ts.map +1 -1
  224. package/dist/dashboard/web/agent-workbench-model.js.map +1 -1
  225. package/dist/dashboard/web/agent-workbench-page.js +2 -2
  226. package/dist/dashboard/web/agent-workbench-page.js.map +1 -1
  227. package/dist/dashboard/web/agent-workbench-panes.d.ts +59 -4
  228. package/dist/dashboard/web/agent-workbench-panes.d.ts.map +1 -1
  229. package/dist/dashboard/web/agent-workbench-panes.js +493 -85
  230. package/dist/dashboard/web/agent-workbench-panes.js.map +1 -1
  231. package/dist/dashboard/web/agent-workbench-session-list.d.ts +11 -7
  232. package/dist/dashboard/web/agent-workbench-session-list.d.ts.map +1 -1
  233. package/dist/dashboard/web/agent-workbench-session-list.js +24 -31
  234. package/dist/dashboard/web/agent-workbench-session-list.js.map +1 -1
  235. package/dist/dashboard/web/agent-workbench-terminal-control.d.ts +198 -0
  236. package/dist/dashboard/web/agent-workbench-terminal-control.d.ts.map +1 -0
  237. package/dist/dashboard/web/agent-workbench-terminal-control.js +292 -0
  238. package/dist/dashboard/web/agent-workbench-terminal-control.js.map +1 -0
  239. package/dist/dashboard/web/agent-workbench-view.d.ts +3 -2
  240. package/dist/dashboard/web/agent-workbench-view.d.ts.map +1 -1
  241. package/dist/dashboard/web/agent-workbench-view.js +69 -50
  242. package/dist/dashboard/web/agent-workbench-view.js.map +1 -1
  243. package/dist/dashboard/web/app.d.ts.map +1 -1
  244. package/dist/dashboard/web/app.js +58 -7
  245. package/dist/dashboard/web/app.js.map +1 -1
  246. package/dist/dashboard/web/bot-defaults-page.d.ts +24 -0
  247. package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
  248. package/dist/dashboard/web/bot-defaults-page.js +191 -16
  249. package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
  250. package/dist/dashboard/web/bot-defaults.d.ts +17 -0
  251. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  252. package/dist/dashboard/web/bot-defaults.js +51 -2
  253. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  254. package/dist/dashboard/web/bot-onboarding.d.ts.map +1 -1
  255. package/dist/dashboard/web/bot-onboarding.js +32 -8
  256. package/dist/dashboard/web/bot-onboarding.js.map +1 -1
  257. package/dist/dashboard/web/confirm-modal.d.ts +35 -0
  258. package/dist/dashboard/web/confirm-modal.d.ts.map +1 -0
  259. package/dist/dashboard/web/confirm-modal.js +111 -0
  260. package/dist/dashboard/web/confirm-modal.js.map +1 -0
  261. package/dist/dashboard/web/connectors-page.d.ts.map +1 -1
  262. package/dist/dashboard/web/connectors-page.js +2 -1
  263. package/dist/dashboard/web/connectors-page.js.map +1 -1
  264. package/dist/dashboard/web/customization-page.d.ts +3 -0
  265. package/dist/dashboard/web/customization-page.d.ts.map +1 -0
  266. package/dist/dashboard/web/customization-page.js +315 -0
  267. package/dist/dashboard/web/customization-page.js.map +1 -0
  268. package/dist/dashboard/web/dashboard-routes.d.ts.map +1 -1
  269. package/dist/dashboard/web/dashboard-routes.js +1 -0
  270. package/dist/dashboard/web/dashboard-routes.js.map +1 -1
  271. package/dist/dashboard/web/groups-page.d.ts.map +1 -1
  272. package/dist/dashboard/web/groups-page.js +11 -9
  273. package/dist/dashboard/web/groups-page.js.map +1 -1
  274. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  275. package/dist/dashboard/web/i18n.js +226 -42
  276. package/dist/dashboard/web/i18n.js.map +1 -1
  277. package/dist/dashboard/web/monitor-room.d.ts.map +1 -1
  278. package/dist/dashboard/web/monitor-room.js +3 -2
  279. package/dist/dashboard/web/monitor-room.js.map +1 -1
  280. package/dist/dashboard/web/overview-page.d.ts.map +1 -1
  281. package/dist/dashboard/web/overview-page.js +98 -63
  282. package/dist/dashboard/web/overview-page.js.map +1 -1
  283. package/dist/dashboard/web/roles-page.d.ts.map +1 -1
  284. package/dist/dashboard/web/roles-page.js +4 -3
  285. package/dist/dashboard/web/roles-page.js.map +1 -1
  286. package/dist/dashboard/web/schedules-page.d.ts.map +1 -1
  287. package/dist/dashboard/web/schedules-page.js +165 -18
  288. package/dist/dashboard/web/schedules-page.js.map +1 -1
  289. package/dist/dashboard/web/sessions-kanban.d.ts +2 -0
  290. package/dist/dashboard/web/sessions-kanban.d.ts.map +1 -1
  291. package/dist/dashboard/web/sessions-kanban.js +142 -24
  292. package/dist/dashboard/web/sessions-kanban.js.map +1 -1
  293. package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
  294. package/dist/dashboard/web/sessions-page.js +177 -94
  295. package/dist/dashboard/web/sessions-page.js.map +1 -1
  296. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  297. package/dist/dashboard/web/sessions.js +5 -4
  298. package/dist/dashboard/web/sessions.js.map +1 -1
  299. package/dist/dashboard/web/settings-page.d.ts +0 -8
  300. package/dist/dashboard/web/settings-page.d.ts.map +1 -1
  301. package/dist/dashboard/web/settings-page.js +185 -65
  302. package/dist/dashboard/web/settings-page.js.map +1 -1
  303. package/dist/dashboard/web/skills/skill-library-tab.d.ts +2 -0
  304. package/dist/dashboard/web/skills/skill-library-tab.d.ts.map +1 -1
  305. package/dist/dashboard/web/skills/skill-library-tab.js +15 -1
  306. package/dist/dashboard/web/skills/skill-library-tab.js.map +1 -1
  307. package/dist/dashboard/web/skills-page.d.ts.map +1 -1
  308. package/dist/dashboard/web/skills-page.js +5 -4
  309. package/dist/dashboard/web/skills-page.js.map +1 -1
  310. package/dist/dashboard/web/store.d.ts +5 -0
  311. package/dist/dashboard/web/store.d.ts.map +1 -1
  312. package/dist/dashboard/web/store.js +4 -0
  313. package/dist/dashboard/web/store.js.map +1 -1
  314. package/dist/dashboard/web/team-federation-page.d.ts.map +1 -1
  315. package/dist/dashboard/web/team-federation-page.js +3 -2
  316. package/dist/dashboard/web/team-federation-page.js.map +1 -1
  317. package/dist/dashboard/web/toast.d.ts +15 -0
  318. package/dist/dashboard/web/toast.d.ts.map +1 -0
  319. package/dist/dashboard/web/toast.js +70 -0
  320. package/dist/dashboard/web/toast.js.map +1 -0
  321. package/dist/dashboard/web/v3-components.d.ts.map +1 -1
  322. package/dist/dashboard/web/v3-components.js +2 -1
  323. package/dist/dashboard/web/v3-components.js.map +1 -1
  324. package/dist/dashboard/web/vc-consumer-profiles-section.d.ts +10 -12
  325. package/dist/dashboard/web/vc-consumer-profiles-section.d.ts.map +1 -1
  326. package/dist/dashboard/web/vc-consumer-profiles-section.js +219 -200
  327. package/dist/dashboard/web/vc-consumer-profiles-section.js.map +1 -1
  328. package/dist/dashboard/web/whiteboards-page.d.ts.map +1 -1
  329. package/dist/dashboard/web/whiteboards-page.js +2 -1
  330. package/dist/dashboard/web/whiteboards-page.js.map +1 -1
  331. package/dist/dashboard-web/app.js +1 -1
  332. package/dist/dashboard-web/chunks/agent-workbench-dock-page-LZSUOCEQ.js +1 -0
  333. package/dist/dashboard-web/chunks/agent-workbench-page-YONDRJU7.js +1 -0
  334. package/dist/dashboard-web/chunks/bot-defaults-page-X7LV6373.js +1 -0
  335. package/dist/dashboard-web/chunks/chunk-2PVRDXEL.js +1 -0
  336. package/dist/dashboard-web/chunks/chunk-53JUKBU2.js +10 -0
  337. package/dist/dashboard-web/chunks/{chunk-5CJMHTT4.js → chunk-5QX4OYOR.js} +1 -1
  338. package/dist/dashboard-web/chunks/{chunk-NACP7BMR.js → chunk-5VRVJFQR.js} +1 -1
  339. package/dist/dashboard-web/chunks/{chunk-FDZ7WOBW.js → chunk-6ROOZWYV.js} +1 -1
  340. package/dist/dashboard-web/chunks/{chunk-F6KNPF4R.js → chunk-CK22743G.js} +15 -19
  341. package/dist/dashboard-web/chunks/chunk-CVNIMUPY.js +25 -0
  342. package/dist/dashboard-web/chunks/chunk-GJC2TMWS.js +2 -0
  343. package/dist/dashboard-web/chunks/chunk-OT6L6G7R.js +1 -0
  344. package/dist/dashboard-web/chunks/chunk-QITAMHW6.js +1 -0
  345. package/dist/dashboard-web/chunks/chunk-RHVFZC5Z.js +1 -0
  346. package/dist/dashboard-web/chunks/chunk-SOBMN2VY.js +1 -0
  347. package/dist/dashboard-web/chunks/chunk-SZOB6VK2.js +1 -0
  348. package/dist/dashboard-web/chunks/chunk-XVIXNQGC.js +44 -0
  349. package/dist/dashboard-web/chunks/chunk-YUJF34ME.js +1 -0
  350. package/dist/dashboard-web/chunks/connectors-page-LB2HEJ7F.js +1 -0
  351. package/dist/dashboard-web/chunks/customization-page-4GXBYLG6.js +105 -0
  352. package/dist/dashboard-web/chunks/feedback-page-IZQ7VLDG.js +1 -0
  353. package/dist/dashboard-web/chunks/groups-page-4WCP5OEV.js +8 -0
  354. package/dist/dashboard-web/chunks/{insights-page-HJFDYR5N.js → insights-page-TXQHIPQL.js} +1 -1
  355. package/dist/dashboard-web/chunks/{monitor-room-V7PCP47V.js → monitor-room-VBRVWXCM.js} +8 -8
  356. package/dist/dashboard-web/chunks/monitoring-page-QDDZWERE.js +2 -0
  357. package/dist/dashboard-web/chunks/office-page-PENFNYPG.js +1 -0
  358. package/dist/dashboard-web/chunks/overview-page-BZQDP6MP.js +1 -0
  359. package/dist/dashboard-web/chunks/plugin-page-JX4GSWZS.js +1 -0
  360. package/dist/dashboard-web/chunks/roles-page-VPNINZNK.js +1 -0
  361. package/dist/dashboard-web/chunks/schedules-page-BITO6HEA.js +1 -0
  362. package/dist/dashboard-web/chunks/sessions-page-ZKCOVTD2.js +8 -0
  363. package/dist/dashboard-web/chunks/settings-page-UT6JDTS6.js +2 -0
  364. package/dist/dashboard-web/chunks/skills-page-GULXVAJY.js +4 -0
  365. package/dist/dashboard-web/chunks/team-federation-page-CUUJWGEG.js +1 -0
  366. package/dist/dashboard-web/chunks/v3-page-5WVJ5AUR.js +94 -0
  367. package/dist/dashboard-web/chunks/whiteboards-page-FZ2CME4H.js +1 -0
  368. package/dist/dashboard-web/design-tokens.css +236 -0
  369. package/dist/dashboard-web/index.html +1 -0
  370. package/dist/dashboard-web/style.css +1930 -114
  371. package/dist/dashboard.js +360 -279
  372. package/dist/dashboard.js.map +1 -1
  373. package/dist/desktop/renderer/app.js +6 -0
  374. package/dist/desktop/renderer/app.js.map +1 -1
  375. package/dist/dsh-runner.js +139 -17
  376. package/dist/dsh-runner.js.map +1 -1
  377. package/dist/global-config.d.ts +33 -2
  378. package/dist/global-config.d.ts.map +1 -1
  379. package/dist/global-config.js +62 -0
  380. package/dist/global-config.js.map +1 -1
  381. package/dist/i18n/en.d.ts.map +1 -1
  382. package/dist/i18n/en.js +13 -4
  383. package/dist/i18n/en.js.map +1 -1
  384. package/dist/i18n/index.d.ts +27 -3
  385. package/dist/i18n/index.d.ts.map +1 -1
  386. package/dist/i18n/index.js +33 -4
  387. package/dist/i18n/index.js.map +1 -1
  388. package/dist/i18n/zh.d.ts.map +1 -1
  389. package/dist/i18n/zh.js +13 -4
  390. package/dist/i18n/zh.js.map +1 -1
  391. package/dist/im/lark/card-builder.d.ts +2 -0
  392. package/dist/im/lark/card-builder.d.ts.map +1 -1
  393. package/dist/im/lark/card-builder.js +7 -4
  394. package/dist/im/lark/card-builder.js.map +1 -1
  395. package/dist/im/lark/card-handler.d.ts.map +1 -1
  396. package/dist/im/lark/card-handler.js +135 -20
  397. package/dist/im/lark/card-handler.js.map +1 -1
  398. package/dist/im/lark/event-dispatcher.d.ts +38 -3
  399. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  400. package/dist/im/lark/event-dispatcher.js +163 -13
  401. package/dist/im/lark/event-dispatcher.js.map +1 -1
  402. package/dist/im/lark/v3-blocked-card-handler.d.ts.map +1 -1
  403. package/dist/im/lark/v3-blocked-card-handler.js +2 -1
  404. package/dist/im/lark/v3-blocked-card-handler.js.map +1 -1
  405. package/dist/im/lark/v3-blocked-card.d.ts +1 -1
  406. package/dist/im/lark/v3-blocked-card.d.ts.map +1 -1
  407. package/dist/im/lark/v3-blocked-card.js +14 -0
  408. package/dist/im/lark/v3-blocked-card.js.map +1 -1
  409. package/dist/index-core-only.js +2 -0
  410. package/dist/index-core-only.js.map +1 -1
  411. package/dist/index-daemon.js +3 -0
  412. package/dist/index-daemon.js.map +1 -1
  413. package/dist/platform/binding.d.ts +37 -0
  414. package/dist/platform/binding.d.ts.map +1 -1
  415. package/dist/platform/binding.js +15 -0
  416. package/dist/platform/binding.js.map +1 -1
  417. package/dist/services/bridge-turn-journal.d.ts +63 -0
  418. package/dist/services/bridge-turn-journal.d.ts.map +1 -0
  419. package/dist/services/bridge-turn-journal.js +149 -0
  420. package/dist/services/bridge-turn-journal.js.map +1 -0
  421. package/dist/services/bridge-turn-queue.d.ts +8 -1
  422. package/dist/services/bridge-turn-queue.d.ts.map +1 -1
  423. package/dist/services/bridge-turn-queue.js +2 -1
  424. package/dist/services/bridge-turn-queue.js.map +1 -1
  425. package/dist/services/codex-app-runner-protocol.d.ts +5 -0
  426. package/dist/services/codex-app-runner-protocol.d.ts.map +1 -1
  427. package/dist/services/codex-app-runner-protocol.js +5 -0
  428. package/dist/services/codex-app-runner-protocol.js.map +1 -1
  429. package/dist/services/customization-store.d.ts +93 -0
  430. package/dist/services/customization-store.d.ts.map +1 -0
  431. package/dist/services/customization-store.js +542 -0
  432. package/dist/services/customization-store.js.map +1 -0
  433. package/dist/services/file-bridge-path.d.ts.map +1 -1
  434. package/dist/services/file-bridge-path.js +3 -0
  435. package/dist/services/file-bridge-path.js.map +1 -1
  436. package/dist/services/grok-transcript.d.ts.map +1 -1
  437. package/dist/services/grok-transcript.js +44 -9
  438. package/dist/services/grok-transcript.js.map +1 -1
  439. package/dist/services/local-cli-opener.d.ts.map +1 -1
  440. package/dist/services/local-cli-opener.js +12 -3
  441. package/dist/services/local-cli-opener.js.map +1 -1
  442. package/dist/services/model-catalog.d.ts +54 -0
  443. package/dist/services/model-catalog.d.ts.map +1 -0
  444. package/dist/services/model-catalog.js +147 -0
  445. package/dist/services/model-catalog.js.map +1 -0
  446. package/dist/services/omp-transcript.d.ts +27 -0
  447. package/dist/services/omp-transcript.d.ts.map +1 -0
  448. package/dist/services/omp-transcript.js +214 -0
  449. package/dist/services/omp-transcript.js.map +1 -0
  450. package/dist/services/platform-team-store.d.ts +16 -8
  451. package/dist/services/platform-team-store.d.ts.map +1 -1
  452. package/dist/services/platform-team-store.js +17 -10
  453. package/dist/services/platform-team-store.js.map +1 -1
  454. package/dist/services/project-scanner.d.ts.map +1 -1
  455. package/dist/services/project-scanner.js +13 -8
  456. package/dist/services/project-scanner.js.map +1 -1
  457. package/dist/services/resumable-session-discovery.d.ts.map +1 -1
  458. package/dist/services/resumable-session-discovery.js +117 -35
  459. package/dist/services/resumable-session-discovery.js.map +1 -1
  460. package/dist/services/structured-bridge-clis.d.ts +5 -3
  461. package/dist/services/structured-bridge-clis.d.ts.map +1 -1
  462. package/dist/services/structured-bridge-clis.js +5 -1
  463. package/dist/services/structured-bridge-clis.js.map +1 -1
  464. package/dist/services/vc-meeting-action-gate.d.ts.map +1 -1
  465. package/dist/services/vc-meeting-action-gate.js +15 -4
  466. package/dist/services/vc-meeting-action-gate.js.map +1 -1
  467. package/dist/services/vc-meeting-consumer-profile-bootstrap.d.ts +46 -66
  468. package/dist/services/vc-meeting-consumer-profile-bootstrap.d.ts.map +1 -1
  469. package/dist/services/vc-meeting-consumer-profile-bootstrap.js +33 -292
  470. package/dist/services/vc-meeting-consumer-profile-bootstrap.js.map +1 -1
  471. package/dist/services/vc-meeting-consumer-profile-templates.js +3 -3
  472. package/dist/services/vc-meeting-consumer-profile-templates.js.map +1 -1
  473. package/dist/services/vc-meeting-delivery-store.d.ts +18 -0
  474. package/dist/services/vc-meeting-delivery-store.d.ts.map +1 -1
  475. package/dist/services/vc-meeting-delivery-store.js +55 -0
  476. package/dist/services/vc-meeting-delivery-store.js.map +1 -1
  477. package/dist/services/vc-meeting-send-policy.d.ts +21 -1
  478. package/dist/services/vc-meeting-send-policy.d.ts.map +1 -1
  479. package/dist/services/vc-meeting-send-policy.js +30 -2
  480. package/dist/services/vc-meeting-send-policy.js.map +1 -1
  481. package/dist/services/vc-meeting-shared-consumer-catalog-store.d.ts +78 -0
  482. package/dist/services/vc-meeting-shared-consumer-catalog-store.d.ts.map +1 -0
  483. package/dist/services/vc-meeting-shared-consumer-catalog-store.js +227 -0
  484. package/dist/services/vc-meeting-shared-consumer-catalog-store.js.map +1 -0
  485. package/dist/services/vc-meeting-shared-consumer-catalog.d.ts +53 -0
  486. package/dist/services/vc-meeting-shared-consumer-catalog.d.ts.map +1 -0
  487. package/dist/services/vc-meeting-shared-consumer-catalog.js +161 -0
  488. package/dist/services/vc-meeting-shared-consumer-catalog.js.map +1 -0
  489. package/dist/setup/bot-config-editor.d.ts.map +1 -1
  490. package/dist/setup/bot-config-editor.js +1 -0
  491. package/dist/setup/bot-config-editor.js.map +1 -1
  492. package/dist/setup/open-platform-automation.d.ts +20 -0
  493. package/dist/setup/open-platform-automation.d.ts.map +1 -1
  494. package/dist/setup/open-platform-automation.js +27 -0
  495. package/dist/setup/open-platform-automation.js.map +1 -1
  496. package/dist/skills/customization-bundle.d.ts +83 -0
  497. package/dist/skills/customization-bundle.d.ts.map +1 -0
  498. package/dist/skills/customization-bundle.js +214 -0
  499. package/dist/skills/customization-bundle.js.map +1 -0
  500. package/dist/skills/definitions.d.ts.map +1 -1
  501. package/dist/skills/definitions.js +10 -3
  502. package/dist/skills/definitions.js.map +1 -1
  503. package/dist/skills/effective-builtins.d.ts +38 -0
  504. package/dist/skills/effective-builtins.d.ts.map +1 -0
  505. package/dist/skills/effective-builtins.js +96 -0
  506. package/dist/skills/effective-builtins.js.map +1 -0
  507. package/dist/skills/injection-mode.d.ts +4 -1
  508. package/dist/skills/injection-mode.d.ts.map +1 -1
  509. package/dist/skills/injection-mode.js +11 -3
  510. package/dist/skills/injection-mode.js.map +1 -1
  511. package/dist/skills/installer.d.ts.map +1 -1
  512. package/dist/skills/installer.js +24 -1
  513. package/dist/skills/installer.js.map +1 -1
  514. package/dist/skills/prompt-fragments.d.ts +58 -0
  515. package/dist/skills/prompt-fragments.d.ts.map +1 -0
  516. package/dist/skills/prompt-fragments.js +88 -0
  517. package/dist/skills/prompt-fragments.js.map +1 -0
  518. package/dist/types.d.ts +51 -0
  519. package/dist/types.d.ts.map +1 -1
  520. package/dist/utils/cli-usage-limit.d.ts +50 -0
  521. package/dist/utils/cli-usage-limit.d.ts.map +1 -1
  522. package/dist/utils/cli-usage-limit.js +43 -0
  523. package/dist/utils/cli-usage-limit.js.map +1 -1
  524. package/dist/utils/idle-detector.d.ts +12 -0
  525. package/dist/utils/idle-detector.d.ts.map +1 -1
  526. package/dist/utils/idle-detector.js +88 -2
  527. package/dist/utils/idle-detector.js.map +1 -1
  528. package/dist/utils/stuck-detector.d.ts +5 -0
  529. package/dist/utils/stuck-detector.d.ts.map +1 -1
  530. package/dist/utils/stuck-detector.js +41 -0
  531. package/dist/utils/stuck-detector.js.map +1 -1
  532. package/dist/utils/usage-limit-tracker.d.ts +46 -0
  533. package/dist/utils/usage-limit-tracker.d.ts.map +1 -0
  534. package/dist/utils/usage-limit-tracker.js +91 -0
  535. package/dist/utils/usage-limit-tracker.js.map +1 -0
  536. package/dist/utils/user-token.js +1 -1
  537. package/dist/utils/user-token.js.map +1 -1
  538. package/dist/worker.js +812 -125
  539. package/dist/worker.js.map +1 -1
  540. package/dist/workflows/definition.d.ts +14 -14
  541. package/dist/workflows/events/payloads.d.ts +12 -12
  542. package/dist/workflows/events/schema.d.ts +164 -164
  543. package/dist/workflows/migration/v2-to-v3.js +1 -1
  544. package/dist/workflows/migration/v2-to-v3.js.map +1 -1
  545. package/dist/workflows/v3/architect.d.ts.map +1 -1
  546. package/dist/workflows/v3/architect.js +11 -5
  547. package/dist/workflows/v3/architect.js.map +1 -1
  548. package/dist/workflows/v3/artifact-contract-declarations.d.ts +22 -0
  549. package/dist/workflows/v3/artifact-contract-declarations.d.ts.map +1 -0
  550. package/dist/workflows/v3/artifact-contract-declarations.js +95 -0
  551. package/dist/workflows/v3/artifact-contract-declarations.js.map +1 -0
  552. package/dist/workflows/v3/contract.d.ts +1 -0
  553. package/dist/workflows/v3/contract.d.ts.map +1 -1
  554. package/dist/workflows/v3/contract.js.map +1 -1
  555. package/dist/workflows/v3/daemon-run.d.ts +2 -2
  556. package/dist/workflows/v3/daemon-run.d.ts.map +1 -1
  557. package/dist/workflows/v3/daemon-run.js +4 -0
  558. package/dist/workflows/v3/daemon-run.js.map +1 -1
  559. package/dist/workflows/v3/dag.d.ts +7 -0
  560. package/dist/workflows/v3/dag.d.ts.map +1 -1
  561. package/dist/workflows/v3/dag.js +69 -8
  562. package/dist/workflows/v3/dag.js.map +1 -1
  563. package/dist/workflows/v3/ephemeral-pool.js +1 -1
  564. package/dist/workflows/v3/ephemeral-pool.js.map +1 -1
  565. package/dist/workflows/v3/event-contract.d.ts +3 -1
  566. package/dist/workflows/v3/event-contract.d.ts.map +1 -1
  567. package/dist/workflows/v3/host.d.ts.map +1 -1
  568. package/dist/workflows/v3/host.js +5 -1
  569. package/dist/workflows/v3/host.js.map +1 -1
  570. package/dist/workflows/v3/journal.d.ts +1 -1
  571. package/dist/workflows/v3/journal.d.ts.map +1 -1
  572. package/dist/workflows/v3/journal.js.map +1 -1
  573. package/dist/workflows/v3/library-materialize.d.ts.map +1 -1
  574. package/dist/workflows/v3/library-materialize.js +9 -1
  575. package/dist/workflows/v3/library-materialize.js.map +1 -1
  576. package/dist/workflows/v3/library-schema.d.ts +3 -1
  577. package/dist/workflows/v3/library-schema.d.ts.map +1 -1
  578. package/dist/workflows/v3/library-schema.js +20 -4
  579. package/dist/workflows/v3/library-schema.js.map +1 -1
  580. package/dist/workflows/v3/library-store.d.ts.map +1 -1
  581. package/dist/workflows/v3/library-store.js +15 -3
  582. package/dist/workflows/v3/library-store.js.map +1 -1
  583. package/dist/workflows/v3/run-envelope.d.ts +16 -16
  584. package/dist/workflows/v3/shared-node-runtime.d.ts +2 -1
  585. package/dist/workflows/v3/shared-node-runtime.d.ts.map +1 -1
  586. package/dist/workflows/v3/shared-node-runtime.js +48 -7
  587. package/dist/workflows/v3/shared-node-runtime.js.map +1 -1
  588. package/package.json +2 -1
  589. package/dist/dashboard-web/chunks/agent-workbench-dock-page-IKAC5CFL.js +0 -1
  590. package/dist/dashboard-web/chunks/agent-workbench-page-M53UN5EB.js +0 -1
  591. package/dist/dashboard-web/chunks/bot-defaults-page-YKE5SDZF.js +0 -10
  592. package/dist/dashboard-web/chunks/chunk-22WIADUT.js +0 -2
  593. package/dist/dashboard-web/chunks/chunk-24ONYQMR.js +0 -1
  594. package/dist/dashboard-web/chunks/chunk-2W6SCN3M.js +0 -1
  595. package/dist/dashboard-web/chunks/chunk-4XNJX6B3.js +0 -1
  596. package/dist/dashboard-web/chunks/chunk-5THX4DI2.js +0 -66
  597. package/dist/dashboard-web/chunks/chunk-7SZRL6FY.js +0 -1
  598. package/dist/dashboard-web/chunks/chunk-AGCPF6MT.js +0 -1
  599. package/dist/dashboard-web/chunks/chunk-FXW3V4JR.js +0 -1
  600. package/dist/dashboard-web/chunks/chunk-SYC6D5XU.js +0 -1
  601. package/dist/dashboard-web/chunks/connectors-page-VOISXXII.js +0 -1
  602. package/dist/dashboard-web/chunks/feedback-page-RN6HRZMS.js +0 -1
  603. package/dist/dashboard-web/chunks/groups-page-UQT6DCB7.js +0 -8
  604. package/dist/dashboard-web/chunks/monitoring-page-NKEKVQAP.js +0 -2
  605. package/dist/dashboard-web/chunks/office-page-GVJF73XW.js +0 -1
  606. package/dist/dashboard-web/chunks/overview-page-KHLV6VTI.js +0 -1
  607. package/dist/dashboard-web/chunks/plugin-page-J3OF5IY4.js +0 -1
  608. package/dist/dashboard-web/chunks/roles-page-WQIRSOLZ.js +0 -1
  609. package/dist/dashboard-web/chunks/schedules-page-J2DEY2X6.js +0 -1
  610. package/dist/dashboard-web/chunks/sessions-page-7BQFEA5P.js +0 -8
  611. package/dist/dashboard-web/chunks/settings-page-BJEIOFJH.js +0 -2
  612. package/dist/dashboard-web/chunks/skills-page-HGIFR3IU.js +0 -4
  613. package/dist/dashboard-web/chunks/team-federation-page-MR5ZPL4B.js +0 -1
  614. package/dist/dashboard-web/chunks/v3-page-GBR5JWPE.js +0 -94
  615. package/dist/dashboard-web/chunks/whiteboards-page-QAZM4BN3.js +0 -1
  616. package/dist/services/vc-meeting-consumer-profile-store.d.ts +0 -39
  617. package/dist/services/vc-meeting-consumer-profile-store.d.ts.map +0 -1
  618. package/dist/services/vc-meeting-consumer-profile-store.js +0 -191
  619. package/dist/services/vc-meeting-consumer-profile-store.js.map +0 -1
package/dist/worker.js CHANGED
@@ -15,15 +15,16 @@
15
15
  import { createHash, randomBytes } from 'node:crypto';
16
16
  import { accessSync, chmodSync, mkdirSync, writeFileSync, unlinkSync, rmdirSync, existsSync, statSync, lstatSync, readdirSync, readlinkSync, readFileSync, realpathSync, copyFileSync, watch as fsWatch, createWriteStream, constants as fsConstants } from 'node:fs';
17
17
  import { atomicWriteFileSync } from './utils/atomic-write.js';
18
- import { join, basename, dirname, delimiter } from 'node:path';
18
+ import { join, basename, dirname, delimiter, relative } from 'node:path';
19
19
  import { resolveBotmuxWrapperBinDir, prependBotmuxBin } from './core/botmux-wrapper.js';
20
20
  import { homedir, tmpdir, userInfo } from 'node:os';
21
21
  import { spawnSync } from 'node:child_process';
22
22
  import { evaluateCredentialOnlyIsolationGate, credentialIsolationRequired, deviceCredentialIsolationMarkerPath, isCredentialIsolationReservedBasename, buildCredentialIsolationRules, buildSeatbeltProfile, isolatedPaneOriginChannel, isolatedPaneReattachSafe, evaluatePersistentPaneMigration, executePersistentPaneMigration, persistentTeardownKillKind, isolationPaneMarkerPath, policyOffTombstonePath, policyOffTombstoneContent, policyOffTombstoneValid, provenancePendingContent, provenancePendingNonce, sendCredFilePath, botHomePath, buildCliExecutableReadCarveOuts, isolationPaneMarkerContent, isolationPanePolicyDigest, } from './adapters/cli/read-isolation.js';
23
- import { buildFsPolicy, compileToSeatbelt, migrateLegacySandboxFields, resolveRedirectedAdapterAuthPaths, FsPolicyConfigError } from './adapters/cli/fs-policy.js';
23
+ import { buildFsPolicy, compileToSeatbelt, migrateLegacySandboxFields, resolveRedirectedAdapterAuthPaths, resolveLarkCliLinuxStoreDir, larkCliChildDataRoot, FsPolicyConfigError } from './adapters/cli/fs-policy.js';
24
24
  import { buildCloseResultMessage, normalizeDestroyResult, mayRestoreWriteAdmission, interpretAbortOutcome, classifyRestartTeardown } from './adapters/backend/destroy-result.js';
25
25
  import { isRemoteBackendType, killPersistentBackendTarget, killPersistentSession, probePersistentBackendTarget, probePersistentSession, shouldRejectPersistentPostKillProbe } from './core/persistent-backend.js';
26
26
  import { finalizeRawCommandDelivery, writeRawCommandLine } from './core/raw-command-writer.js';
27
+ import { decodeTerminalWriteFrameSource, terminalWriteFrame, } from './core/terminal-write-frame.js';
27
28
  import { rawCommandWriteOptionsFor } from './core/raw-command-write-options.js';
28
29
  import { publishCliSessionIdToDaemon } from './core/cli-session-id-publisher.js';
29
30
  import { readProcessStartIdentity } from './core/session-marker.js';
@@ -46,20 +47,24 @@ import { remoteWorkerShutdownInputBlocker } from './core/remote-worker-shutdown-
46
47
  import { ReadyGate, shouldArmReadyGate } from './utils/ready-gate.js';
47
48
  import { shouldRunStartupCommandsOnSpawn, shouldDeferInitialPromptForStartup } from './core/startup-commands.js';
48
49
  import { sanitizePerBotEnv } from './core/per-bot-env.js';
50
+ import { normalizeExistingAppServerEndpoint } from './core/existing-app-server.js';
49
51
  import { resolveChildBotsConfig } from './core/config-dir.js';
50
52
  import { evaluateVcMeetingManagedSend, } from './services/vc-meeting-send-policy.js';
51
53
  import { TurnTerminalDeduper } from './services/turn-terminal-deduper.js';
54
+ import { appendBridgeTurnJournalEntry, BridgeRestoreGate, clearBridgeTurnJournal, readBridgeTurnJournal, removeBridgeTurnJournalEntry, selectRestorableBridgeTurns, writeBridgeTurnJournal, } from './services/bridge-turn-journal.js';
52
55
  import { defaultGatewayEntry, ensureGatewayEntry } from './core/plugins/mcp/gateway-installer.js';
53
- import { sessionMcpGatewayPathRegex, startSessionMcpGatewayHost, } from './core/plugins/mcp/host.js';
56
+ import { sessionMcpGatewayPathRegex, sessionMcpGatewaySocketPath, startSessionMcpGatewayHost, } from './core/plugins/mcp/host.js';
57
+ import { clearMcpGatewayLaunchRecord, mcpGatewayPaneReattachSafe, readMcpGatewayLaunchRecord, writeMcpGatewayLaunchRecord, } from './core/plugins/mcp/launch-record.js';
54
58
  import { MCP_GATEWAY_REQUIRED_ENV, MCP_GATEWAY_SOCKET_ENV, } from './core/plugins/mcp/environment.js';
55
59
  import { readSessionMcpRuntimeManifest, sessionMcpRuntimeHostOnlyPaths, } from './core/plugins/mcp/session-runtime.js';
56
60
  import { prepareCliPluginGeneration } from './core/plugins/cli-generation.js';
57
61
  import { loadBotConfigs, resolveBrandLabel, resolveUsageDisplay, } from './bot-registry.js';
58
62
  import { readGlobalConfig, isWorkflowFeatureEnabled } from './global-config.js';
59
- import { deriveTerminalWriteToken, resolveTerminalAccessForRequest, safeTerminalTokenEqual, } from './core/terminal-write-auth.js';
63
+ import { deriveTerminalWriteToken, resolveTerminalAccessForRequest, safeTerminalTokenEqual, TERMINAL_PLATFORM_READONLY_HINT_HEADER, } from './core/terminal-write-auth.js';
60
64
  import { deriveWorkerViewGeneration, looksLikeTerminalControlGrant, TERMINAL_VIEW_FORWARD_HEADER, verifyTerminalControlGrant, verifyTerminalViewForward, } from './core/terminal-control-grant.js';
61
65
  import { appendControlAudit, controlAuditRecord } from './dashboard/control-audit.js';
62
66
  import { readPlatformBinding } from './platform/binding.js';
67
+ import { buildPlatformDashboardLoginUrl } from './core/dashboard-url.js';
63
68
  import { loadDashboardSecret, loadPersistedToken } from './dashboard/auth.js';
64
69
  import { InflightInputTracker } from './core/inflight-input-tracker.js';
65
70
  import { InputTurnDeduper } from './core/input-turn-dedupe.js';
@@ -78,6 +83,7 @@ import { currentHermesStateOffset, drainHermesStateDb, resolveHermesStateDbPath
78
83
  import { filterHermesEventsForBotmuxSession } from './services/hermes-session-filter.js';
79
84
  import { currentMtrSessionOffset, drainMtrSession, findLatestMtrSessionByDirectory, findMtrSessionById } from './services/mtr-transcript.js';
80
85
  import { drainPiTranscript } from './services/pi-transcript.js';
86
+ import { drainOmpTranscript } from './services/omp-transcript.js';
81
87
  import { drainGrokUpdates, findGrokSessionByPid, grokSessionIdFromPath, } from './services/grok-transcript.js';
82
88
  import { resolveFileBridgePath } from './services/file-bridge-path.js';
83
89
  import { isStructuredBridgeAdoptIdleCli, isStructuredBridgeAdoptInputCli, isStructuredBridgeFallbackActive, isStructuredBridgeLifecycleBlockingCli, } from './services/structured-bridge-clis.js';
@@ -93,6 +99,7 @@ import { listenWebTerminalWithFallback } from './utils/web-terminal-listen.js';
93
99
  import { HerdrWebTerminalBinding } from './utils/herdr-web-terminal-binding.js';
94
100
  import { TERMINAL_FAVICON_DATA_URI } from './utils/terminal-favicon.js';
95
101
  import { t, setDefaultLocale } from './i18n/index.js';
102
+ import { registerPromptOverrideResolver } from './skills/effective-builtins.js';
96
103
  import { TerminalRenderer } from './utils/terminal-renderer.js';
97
104
  import { DEFAULT_RENDER_COLS, DEFAULT_RENDER_ROWS, MAX_RENDER_COLS, MAX_RENDER_ROWS, MIN_RENDER_COLS, MIN_RENDER_ROWS, clamp, resolveRenderDimensions, } from './utils/render-dimensions.js';
98
105
  import { createCliAdapterSync, locateOnPath } from './adapters/cli/registry.js';
@@ -104,6 +111,7 @@ import { delay } from './utils/timing.js';
104
111
  import { claudeJsonlPathForSession, resolveJsonlFromPid, findOpenClaudeSessionIds, syncClaudeResumeTargetToCwd, DEFAULT_CLAUDE_DATA_DIR } from './adapters/cli/claude-code.js';
105
112
  import { sessionReadyHookCommand } from './adapters/hook-command.js';
106
113
  import { mtrSessionIdForBotmuxSession } from './adapters/cli/mtr.js';
114
+ import { ompSessionDir } from './adapters/cli/oh-my-pi.js';
107
115
  import { strictInputHandle } from './adapters/cli/strict-input-handle.js';
108
116
  import { HerdrBackend } from './adapters/backend/herdr-backend.js';
109
117
  import { TmuxBackend } from './adapters/backend/tmux-backend.js';
@@ -127,7 +135,7 @@ import { tmuxEnv, probeTmuxFunctionalWithRetry } from './setup/ensure-tmux.js';
127
135
  import { probeZmxVersion } from './setup/ensure-zmx.js';
128
136
  import { tmuxRestartJitterMs } from './core/tmux-recovery.js';
129
137
  import { IdleDetector } from './utils/idle-detector.js';
130
- import { StuckDetector, matchHookReviewScreen } from './utils/stuck-detector.js';
138
+ import { StuckDetector, matchHookReviewScreen, shouldHoldInputForHookReview, } from './utils/stuck-detector.js';
131
139
  import { processStuckWarningTuiKeys, shouldRearmStuckDetector } from './utils/stuck-key-guard.js';
132
140
  import { sendTuiKeySequence, submitTuiTextInput } from './utils/tui-input-delivery.js';
133
141
  import { captureToPng } from './utils/screenshot-renderer.js';
@@ -135,7 +143,8 @@ import { snapshotToPng, snapshotToText, shouldCaptureScreen, isScreenSelfDriven
135
143
  import { chooseWebTerminalSeed } from './utils/web-terminal-seed.js';
136
144
  import { mergeHerdrWebSnapshot, renderHerdrWebHistory, } from './utils/herdr-web-history.js';
137
145
  import { parseWorkerRequestUrl } from './utils/worker-http.js';
138
- import { detectCliUsageLimit, usageLimitStateKey, structuredRateLimitState, isStructuredRateLimitAuthoritative } from './utils/cli-usage-limit.js';
146
+ import { structuredRateLimitState, isStructuredRateLimitAuthoritative } from './utils/cli-usage-limit.js';
147
+ import { createUsageLimitTracker } from './utils/usage-limit-tracker.js';
139
148
  import { uploadImageBuffer } from './utils/lark-upload.js';
140
149
  import { applySessionOwnerEnv, redactChildEnv, scrubClaudeSessionMarkerEnv, scrubSessionCliHomeEnv } from './utils/child-env.js';
141
150
  import { decideSubmitConfirmationAction, settleDeferredSubmitConfirmation, settleStaleWriteContinuation, } from './services/submit-confirmation.js';
@@ -150,7 +159,7 @@ import { fetchDaemonIpc } from './core/daemon-ipc-auth.js';
150
159
  import { withCodexAppContext } from './utils/codex-app-context.js';
151
160
  import { resolveCodexAppFinalTurnIdentity } from './adapters/cli/codex-app-turn.js';
152
161
  import { RunnerControlDecoder } from './adapters/cli/runner-control-channel.js';
153
- import { normalizeCodexAppLifecycleEvent, normalizeFinalUsage, } from './services/codex-app-runner-protocol.js';
162
+ import { CODEX_APP_ACTIVE_WRITER_EXIT_CODE, normalizeCodexAppLifecycleEvent, normalizeFinalUsage, } from './services/codex-app-runner-protocol.js';
154
163
  import { hasMatchingManagedOriginCapability, ensureManagedOriginAttestationDirectory, ensureManagedOriginCapabilityLeafSafe, ensureManagedOriginDataRootProbe, ensureManagedOriginIsolationSentinel, ensureManagedOriginRootLocator, managedOriginLegacyIsolationProbeAccess, managedOriginDataRootProbeAccess, managedOriginIsolationSentinelAccess, managedOriginAttestationDirectory, managedOriginCapabilityDirectory, managedOriginCapabilityPath, managedOriginRootLocatorPath, readManagedOriginAuthorityFile, RELAY_ORIGIN_CAPABILITY_BASENAME, replaceManagedOriginCapabilityFile, sweepManagedOriginAttestationProofs, } from './core/managed-origin-capability.js';
155
164
  import { CodexRpcEngine, } from './codex-rpc-engine.js';
156
165
  import { beginRuntimeWriteCycle, isCliBackendGenerationCurrent, PtyOutputGeneration, projectRuntimeScreenStatus, } from './utils/runtime-screen-status.js';
@@ -1172,16 +1181,38 @@ async function prepareCliPluginGenerationAndGateway(cfg, adapter) {
1172
1181
  const manifest = readSessionMcpRuntimeManifest(cfg.sessionId, config.session.dataDir);
1173
1182
  stopSessionMcpGatewayHost();
1174
1183
  if (adapter.mcpGateway && manifest?.entries.length) {
1175
- sessionMcpGatewayHost = await startSessionMcpGatewayHost({
1176
- sessionId: cfg.sessionId,
1177
- dataDir: config.session.dataDir,
1178
- trustedTurnIdentity: currentGatewayTrustedTurnIdentity,
1179
- onError: error => log(`[mcp-gateway] host error: ${error.message}`),
1180
- });
1184
+ await startAndRecordSessionMcpGatewayHost(cfg.sessionId);
1181
1185
  log(`[mcp-gateway] trusted host listening for ${manifest.entries.length} plugin server(s)`);
1182
1186
  }
1187
+ else {
1188
+ clearMcpGatewayLaunchRecord(config.session.dataDir, cfg.sessionId);
1189
+ }
1183
1190
  return manifest;
1184
1191
  }
1192
+ /** Bind the trusted MCP Gateway host at the session's DETERMINISTIC socket path
1193
+ * and persist its launch record. Deliberately does NOT refresh the plugin/Skill
1194
+ * generation: prepareCliPluginGenerationAndGateway does that for fresh/resumed
1195
+ * spawns, whereas a warm reattach to a surviving pane keeps the CLI's existing
1196
+ * catalog untouched — but the host itself died with the previous worker, so the
1197
+ * reattach path must still re-serve it here so the pane's surviving relay can
1198
+ * reconnect to the same path (see spawnCli's paneRelayReattachSafe branch).
1199
+ * The caller owns stopping any prior host first (prepare*) or gating on absence
1200
+ * (reattach). Token rotation + stale-socket removal are handled inside
1201
+ * startSessionMcpGatewayHost. The trusted-turn-identity provider (#917) is
1202
+ * wired here so BOTH the fresh/resumed host and the re-served reattach host
1203
+ * inject the per-turn host-signed caller — the reattach path must not lose it. */
1204
+ async function startAndRecordSessionMcpGatewayHost(sessionId) {
1205
+ sessionMcpGatewayHost = await startSessionMcpGatewayHost({
1206
+ sessionId,
1207
+ dataDir: config.session.dataDir,
1208
+ trustedTurnIdentity: currentGatewayTrustedTurnIdentity,
1209
+ onError: error => log(`[mcp-gateway] host error: ${error.message}`),
1210
+ });
1211
+ // The NEXT worker generation reads this record to decide whether a surviving
1212
+ // persistent pane can be reattached (relay reconnects to the same path)
1213
+ // instead of killed for a cold-resume.
1214
+ writeMcpGatewayLaunchRecord(config.session.dataDir, sessionId, sessionMcpGatewayHost.socketPath);
1215
+ }
1185
1216
  /** v2 read isolation — provision a bot's PER-BOT config dir under its BOT_HOME so the
1186
1217
  * CLI (redirected there via CLAUDE_CONFIG_DIR/CODEX_HOME) starts fully set up despite
1187
1218
  * the global ~/.claude|~/.codex being Seatbelt-denied. Idempotent (guards on
@@ -1385,6 +1416,7 @@ const adoptWriteQueue = new AsyncSerialQueue();
1385
1416
  * adapter's hermesBridgeAttach reads the correct mode. */
1386
1417
  let lastSpawnEffectiveResume = false;
1387
1418
  let lastSpawnEffectiveCliSessionId;
1419
+ let lastSpawnEffectiveAdapterSessionId;
1388
1420
  let lastSpawnDeferInitialPrompt = false;
1389
1421
  let lastSpawnQueuedInitialPrompt;
1390
1422
  let lastSpawnQueuedInitialPromptLogicalContent;
@@ -1534,6 +1566,18 @@ function refreshTerminalWriteToken() {
1534
1566
  if (secret && sessionId)
1535
1567
  writeToken = deriveTerminalWriteToken(secret, sessionId);
1536
1568
  }
1569
+ /**
1570
+ * Owner-login URL for THIS session's read-only web terminal, or '' when the
1571
+ * machine is unbound / remote access is off (banner then shows non-link text).
1572
+ * `next=/s/<sessionId>` is what makes the round-trip land the owner back on a
1573
+ * writable terminal: the platform routes a `/s/`-prefixed next to the terminal
1574
+ * subdomain surface and mints the host-only proxy-session cookie there, which
1575
+ * is the credential the owner write-check reads (the cross-subdomain SSO cookie
1576
+ * alone does not). See buildPlatformDashboardLoginUrl.
1577
+ */
1578
+ function deriveTerminalLoginUrl(sid) {
1579
+ return buildPlatformDashboardLoginUrl(`/s/${encodeURIComponent(sid)}`) ?? '';
1580
+ }
1537
1581
  function resolveTerminalAccessForReq(req, url) {
1538
1582
  // P1-6 invariant: an explicit, matching write-link `?token=` is an
1539
1583
  // independent capability with the HIGHEST priority. It wins outright — before
@@ -1641,7 +1685,7 @@ let closeRequested = false;
1641
1685
  let capturedSpawnCommand = null;
1642
1686
  let deferredTopicOutputTail = '';
1643
1687
  const reportedDeferredTopicRoots = new Set();
1644
- const CLI_DISPLAY_NAMES = { 'claude-code': 'Claude', seed: 'Seed', relay: 'Relay', aiden: 'Aiden', coco: 'CoCo', codex: 'Codex', 'codex-app': 'Codex App', cursor: 'Cursor', gemini: 'Gemini', genius: 'Genius', opencode: 'OpenCode', opencode2: 'OpenCode 2', antigravity: 'Antigravity', mtr: 'MTR', hermes: 'Hermes', mira: 'Mira', mir: 'Mir CLI', traex: 'TRAE', pi: 'Pi', copilot: 'Copilot', 'oh-my-pi': 'Oh My Pi', kimi: 'Kimi', grok: 'Grok Build', 'kiro-cli': 'Kiro', riff: 'Riff', reasonix: 'Reasonix', dsh: 'DeepSeek Harness', mojo: 'Mojo' };
1688
+ const CLI_DISPLAY_NAMES = { 'claude-code': 'Claude', seed: 'Seed', relay: 'Relay', aiden: 'Aiden', coco: 'CoCo', codex: 'Codex', 'codex-app': 'Codex App', cursor: 'Cursor', gemini: 'Gemini', genius: 'Genius', opencode: 'OpenCode', opencode2: 'OpenCode 2', antigravity: 'Antigravity', mtr: 'MTR', hermes: 'Hermes', mira: 'Mira', mir: 'Mir CLI', traex: 'TRAE', pi: 'Pi', copilot: 'Copilot', 'oh-my-pi': 'Oh My Pi', kimi: 'Kimi', grok: 'Grok Build', 'kiro-cli': 'Kiro', riff: 'Riff', reasonix: 'Reasonix', dsh: 'DeepSeek Harness', 'dsh-tui': 'DeepSeek Harness TUI', mojo: 'Mojo' };
1645
1689
  function cliName() {
1646
1690
  return (lastInitConfig?.cliRuntime?.source === 'configured'
1647
1691
  ? (lastInitConfig.cliRuntime.displayName?.trim() || lastInitConfig.cliRuntime.id)
@@ -1756,6 +1800,38 @@ let readyPatternSeenDuringHold = false;
1756
1800
  let awaitingPostSessionStartPromptEvidence = false;
1757
1801
  /** Scoped marker set only by IdleDetector's screen-driven callback. */
1758
1802
  let postSessionStartPromptEvidenceInFlight = false;
1803
+ /** A visible Codex Hook-review screen owns Enter/arrow keys. Keep queued IM
1804
+ * input out of that menu so an automated submit cannot accidentally select
1805
+ * “Trust all” or get consumed as menu navigation. */
1806
+ let hookReviewInputHold = false;
1807
+ let hookReviewInputHoldNotified = false;
1808
+ function refreshHookReviewInputHold(snapshot) {
1809
+ if (!snapshot.trim())
1810
+ return;
1811
+ const blocked = shouldHoldInputForHookReview(snapshot);
1812
+ if (blocked === hookReviewInputHold)
1813
+ return;
1814
+ hookReviewInputHold = blocked;
1815
+ if (blocked) {
1816
+ log('Hook review menu visible — holding queued input until the menu is resolved');
1817
+ return;
1818
+ }
1819
+ hookReviewInputHoldNotified = false;
1820
+ log('Hook review menu cleared — releasing queued input');
1821
+ if (hasPendingInputForFlush())
1822
+ queueMicrotask(() => { void flushPending(); });
1823
+ }
1824
+ function notifyHookReviewInputHold() {
1825
+ if (hookReviewInputHoldNotified)
1826
+ return;
1827
+ hookReviewInputHoldNotified = true;
1828
+ send({
1829
+ type: 'user_notify',
1830
+ turnId: currentBotmuxTurnId,
1831
+ message: '⚠️ Codex 正在显示 Hook 审核菜单。为避免消息误触“Trust all”或被菜单吞掉,'
1832
+ + 'BotMux 已暂存后续消息;请先在终端完成 Hook 选择,恢复普通输入框后会自动投递。',
1833
+ });
1834
+ }
1759
1835
  /** Wait until the PTY has been quiet for READY_FLUSH_SETTLE_MS (Ink render
1760
1836
  * drained), capped at READY_FLUSH_SETTLE_CAP_MS, then flush the held prompt.
1761
1837
  * An authoritative direct ready command (Hermes) can mark prompt readiness;
@@ -1991,6 +2067,15 @@ let pendingSessionRename = null;
1991
2067
  let nativeSessionTitleSyncInFlight;
1992
2068
  let nativeSessionTitleAppliedThreadId;
1993
2069
  let nativeSessionTitleRevision = 0;
2070
+ /** Every user-controlled input class that flushPending can deliver. Keep this
2071
+ * shared with review-menu release handling so a queued raw command, adopt
2072
+ * message, or native rename is not stranded after the menu disappears. */
2073
+ function hasPendingInputForFlush() {
2074
+ return pendingMessages.length > 0
2075
+ || pendingAdoptMessages.length > 0
2076
+ || pendingRawInputs.length > 0
2077
+ || pendingSessionRename !== null;
2078
+ }
1994
2079
  let nativeSessionTitleResumeUpdatedAt;
1995
2080
  let nativeSessionTitleCurrentGenerationResume = false;
1996
2081
  const nativeSessionTitleSyncAbortControllers = new Set();
@@ -3379,59 +3464,27 @@ function codexAppLivenessStatus(base, nowMs = Date.now()) {
3379
3464
  function structuredRateLimitAuthoritative() {
3380
3465
  return isStructuredRateLimitAuthoritative(cliAdapter);
3381
3466
  }
3382
- // Per-turn usage-limit state machine. Owns the turn counter plus the
3383
- // "did this turn hit a limit" / "suppress a stale retry-ready banner" flags, so
3384
- // classify()'s state writes are explicit method calls rather than hidden
3385
- // mutations of module globals from a function that otherwise reads as a pure
3386
- // mapper.
3387
- function createUsageLimitTracker() {
3388
- let turnSeq = 0;
3389
- let detectedTurn;
3390
- let suppressedRetryReadyKey;
3391
- return {
3392
- currentTurn() {
3393
- return turnSeq;
3394
- },
3395
- // Open a new turn; remember any stale retry-ready banner still on screen so
3396
- // classify() doesn't re-flag it as a fresh limit this turn.
3397
- beginTurn(snapshot) {
3398
- turnSeq++;
3399
- detectedTurn = undefined;
3400
- const current = detectCliUsageLimit(snapshot, undefined, { suppressRateKind: structuredRateLimitAuthoritative() });
3401
- suppressedRetryReadyKey = current.limited && current.retryReady
3402
- ? usageLimitStateKey(current)
3403
- : undefined;
3404
- return turnSeq;
3405
- },
3406
- // Map a runtime status to a usage-limit-aware status, recording whether this
3407
- // turn hit a limit (read back via detectedThisTurn).
3408
- classify(content, status) {
3409
- const detected = detectCliUsageLimit(content, undefined, { suppressRateKind: structuredRateLimitAuthoritative() });
3410
- if (!detected.limited)
3411
- return { status };
3412
- const key = usageLimitStateKey(detected);
3413
- if (detected.retryReady && key === suppressedRetryReadyKey) {
3414
- return { status };
3415
- }
3416
- suppressedRetryReadyKey = undefined;
3417
- detectedTurn = turnSeq;
3418
- return { status: 'limited', usageLimit: detected };
3419
- },
3420
- detectedThisTurn(seq) {
3421
- return detectedTurn === seq;
3422
- },
3423
- // Record a limit that came from a STRUCTURED signal (transcript error
3424
- // record) rather than screen text. Mirrors classify()'s state writes so
3425
- // the tracker stays coherent: mark this turn as having hit a limit (read
3426
- // by detectedThisTurn for the submit-confirmation recheck) and clear any
3427
- // stale retry-ready suppression. The actual emit is done by the caller.
3428
- noteStructuredLimit() {
3429
- suppressedRetryReadyKey = undefined;
3430
- detectedTurn = turnSeq;
3431
- },
3432
- };
3433
- }
3434
- const usageLimitTracker = createUsageLimitTracker();
3467
+ /**
3468
+ * PTY-quiescence window for the usage-limit active-work gate. A `working`
3469
+ * status alone does not prove output is progressing (it is the default
3470
+ * projection whenever promptReady === false), so the screen-scan suppression
3471
+ * only applies while PTY output is this fresh. A non-structured CLI blocked at
3472
+ * a rate/quota error screen that never renders its ready prompt goes silent
3473
+ * past this window and is correctly detected instead of being suppressed
3474
+ * forever. Aligns with the StuckDetector's PTY-quiescence threshold.
3475
+ */
3476
+ const USAGE_LIMIT_OUTPUT_ACTIVE_WINDOW_MS = 15_000;
3477
+ function isOutputActivelyProgressing() {
3478
+ return Date.now() - lastPtyActivityAtMs < USAGE_LIMIT_OUTPUT_ACTIVE_WINDOW_MS;
3479
+ }
3480
+ // Per-turn usage-limit state machine (extracted to utils/usage-limit-tracker
3481
+ // for unit testing). The structured-limit stickiness it owns is load-bearing:
3482
+ // a one-shot transcript emit must survive the daemon's working-frame self-heal
3483
+ // while the CLI stays genuinely blocked.
3484
+ const usageLimitTracker = createUsageLimitTracker({
3485
+ isRateKindSuppressed: structuredRateLimitAuthoritative,
3486
+ isOutputActive: isOutputActivelyProgressing,
3487
+ });
3435
3488
  function currentUsageLimitSnapshot() {
3436
3489
  if (!backendScreenEvidenceIsAuthoritativeForMutation())
3437
3490
  return '';
@@ -3505,6 +3558,19 @@ const bridgeSecondaryPaths = new Map(); // path → offset
3505
3558
  let bridgeOffset = 0;
3506
3559
  let bridgePendingTail = '';
3507
3560
  const bridgeQueue = new BridgeTurnQueue();
3561
+ /** Journal-restore of interrupted Lark turns is allowed AT MOST ONCE per worker
3562
+ * process, and only on the FIRST baseline this process runs. Rationale: the
3563
+ * journal exists to recover a turn that a *cross-process* death (daemon
3564
+ * restart / worker crash) orphaned — there the whole worker is new and no
3565
+ * other recovery owner survives. An *in-worker* CLI restart (crash-respawn,
3566
+ * cwd-move, durable lease expiry) keeps this process alive, so its
3567
+ * InflightInputTracker carryover already re-delivers the interrupted plain-IM
3568
+ * input (a full fresh answer) — letting the journal ALSO restore that turn
3569
+ * would post the recovered partial AND the fresh answer (same turnId, different
3570
+ * lastUuid → daemon dedupe can't collapse them). Armed on every watcher
3571
+ * teardown so restore is confined to the one baseline with no competing
3572
+ * recovery owner. */
3573
+ const bridgeRestoreGate = new BridgeRestoreGate();
3508
3574
  /** uuids of structured transcript rate-limit records already turned into a
3509
3575
  * `limited` emit. Readers may re-read from offset 0 after rotation, so the
3510
3576
  * stable record uuid keeps the notification idempotent. */
@@ -3538,6 +3604,10 @@ let activeRuntimePublished = false;
3538
3604
  const codexBridgeQueue = new CodexBridgeQueue();
3539
3605
  let codexBridgeWatcher = null;
3540
3606
  let codexBridgeTimer = null;
3607
+ let ompBridgeState = {};
3608
+ let ompQuietCandidateKey;
3609
+ let ompQuietCandidateCompleteOffset;
3610
+ const ompRetiredTranscriptPaths = new Set();
3541
3611
  /** Settings are observed on the same append-only cursor as bridge output.
3542
3612
  * The tracker owns rollout-generation clear/update semantics and publishes a
3543
3613
  * dedicated IPC event, independent of PTY redraw frequency. */
@@ -3666,6 +3736,46 @@ function bridgeMarkerPath() {
3666
3736
  return undefined;
3667
3737
  return join(process.env.SESSION_DATA_DIR, 'turn-sends', `${sessionId}.jsonl`);
3668
3738
  }
3739
+ /** Durable journal of pending Lark bridge turns (services/bridge-turn-journal).
3740
+ * Written at mark time, cleared at every turn-terminal outcome; consulted by
3741
+ * bridgeAbsorbBaseline after a restart so an interrupted turn's output can
3742
+ * still reach Lark. Sibling of the turn-sends marker file. */
3743
+ function bridgeTurnJournalFilePath() {
3744
+ if (!process.env.SESSION_DATA_DIR || !sessionId)
3745
+ return undefined;
3746
+ return join(process.env.SESSION_DATA_DIR, 'turn-marks', `${sessionId}.json`);
3747
+ }
3748
+ function journalBridgeTurnMark(entry) {
3749
+ const path = bridgeTurnJournalFilePath();
3750
+ if (!path)
3751
+ return;
3752
+ try {
3753
+ appendBridgeTurnJournalEntry(path, entry);
3754
+ }
3755
+ catch (err) {
3756
+ log(`Bridge turn journal write failed (${err.message}) — restart recovery unavailable for turn ${entry.turnId.substring(0, 8)}`);
3757
+ }
3758
+ }
3759
+ function journalBridgeTurnClear(turnId, dispatchAttempt) {
3760
+ const path = bridgeTurnJournalFilePath();
3761
+ if (!path)
3762
+ return;
3763
+ try {
3764
+ removeBridgeTurnJournalEntry(path, turnId, dispatchAttempt);
3765
+ }
3766
+ catch (err) {
3767
+ log(`Bridge turn journal clear failed (${err.message}) for turn ${turnId.substring(0, 8)}`);
3768
+ }
3769
+ }
3770
+ function clearBridgeTurnJournalFile() {
3771
+ const path = bridgeTurnJournalFilePath();
3772
+ if (!path)
3773
+ return;
3774
+ try {
3775
+ clearBridgeTurnJournal(path);
3776
+ }
3777
+ catch { /* best-effort — session is closing */ }
3778
+ }
3669
3779
  function readSendMarkers() {
3670
3780
  if (closeRequested)
3671
3781
  return [];
@@ -3916,6 +4026,13 @@ function bridgeAbsorbBaseline() {
3916
4026
  if (!bridgeJsonlPath)
3917
4027
  return;
3918
4028
  if (!lastInitConfig?.adoptMode) {
4029
+ // Restart recovery: if the previous generation left pending Lark turns in
4030
+ // the durable journal (worker/daemon died mid-turn), re-mark them and
4031
+ // baseline BEHIND their recorded offsets instead of skipping to EOF — the
4032
+ // interrupted turn's user line and partial assistant output are then
4033
+ // re-attributed and delivered through the normal fallback gate.
4034
+ if (tryRestoreInterruptedBridgeTurns())
4035
+ return;
3919
4036
  const cursor = baselineJsonlCursor(bridgeJsonlPath);
3920
4037
  bridgeOffset = cursor.newOffset;
3921
4038
  bridgePendingTail = cursor.pendingTail;
@@ -3935,6 +4052,72 @@ function bridgeAbsorbBaseline() {
3935
4052
  if (lastInitConfig?.adoptMode)
3936
4053
  maybeEmitAdoptPreamble(result.events);
3937
4054
  }
4055
+ /** Restore journal-persisted pending turns after a restart interrupted them.
4056
+ *
4057
+ * Returns true when it established the bridge cursor itself (restore mode);
4058
+ * false when there is nothing to restore and the caller should run the
4059
+ * normal baseline-to-EOF.
4060
+ *
4061
+ * Scope guards (all deliberate):
4062
+ * - non-adopt only (caller gates);
4063
+ * - same transcript file only — a sessionId rotation between generations
4064
+ * invalidates offsets and risks cross-file mis-attribution;
4065
+ * - bounded age (BRIDGE_TURN_RESTORE_MAX_AGE_MS) — a partial answer from
4066
+ * days ago is noise, not recovery.
4067
+ *
4068
+ * Safety properties:
4069
+ * - events before the earliest mark (minus the same 5s guard the
4070
+ * fingerprint switch uses) are ABSORBED as history, so old turns are
4071
+ * never re-emitted;
4072
+ * - a stray prior-turn assistant tail after the cutoff can only synthesize
4073
+ * a local turn, which the non-adopt emit gate always suppresses;
4074
+ * - a restored mark whose user line never landed in the jsonl simply
4075
+ * never starts, and pruneExpired clears it (journal entry included) on
4076
+ * the first idle tick;
4077
+ * - the send-marker gate still applies at emit — turn-sends markers
4078
+ * persist across restarts, so an answer the model already `botmux
4079
+ * send`-ed before the kill stays suppressed. */
4080
+ function tryRestoreInterruptedBridgeTurns() {
4081
+ // At-most-once, and only on this worker process's FIRST bridge baseline. Any
4082
+ // baseline that runs after a watcher teardown is an in-worker CLI restart
4083
+ // whose InflightInputTracker carryover already re-delivers the interrupted
4084
+ // turn — restoring here too would double-deliver (see bridgeRestoreGate,
4085
+ // armed in stopBridgeWatcher).
4086
+ if (!bridgeRestoreGate.mayRestoreOnThisBaseline())
4087
+ return false;
4088
+ const journalPath = bridgeTurnJournalFilePath();
4089
+ if (!journalPath || !bridgeJsonlPath)
4090
+ return false;
4091
+ const entries = readBridgeTurnJournal(journalPath);
4092
+ if (entries.length === 0)
4093
+ return false;
4094
+ const restorable = selectRestorableBridgeTurns(entries, { currentJsonlPath: bridgeJsonlPath });
4095
+ if (restorable.length !== entries.length) {
4096
+ // Entries for rotated-away files or beyond the age cap can never restore —
4097
+ // drop them now so they don't linger across future generations.
4098
+ try {
4099
+ writeBridgeTurnJournal(journalPath, restorable);
4100
+ }
4101
+ catch { /* best-effort */ }
4102
+ }
4103
+ if (restorable.length === 0)
4104
+ return false;
4105
+ for (const entry of restorable) {
4106
+ bridgeQueue.mark(entry.turnId, entry.fingerprint, entry.markTimeMs, entry.contentNormalized, entry.dispatchAttempt, { restoredFromJournal: true });
4107
+ }
4108
+ const fromOffset = Math.min(...restorable.map(entry => entry.offsetAtMark));
4109
+ const drained = drainTranscript(bridgeJsonlPath, fromOffset);
4110
+ bridgeOffset = drained.newOffset;
4111
+ bridgePendingTail = drained.pendingTail;
4112
+ const cutoffMs = Math.min(...restorable.map(entry => entry.markTimeMs)) - 5_000;
4113
+ const { history, live } = splitTranscriptEventsByCutoff(drained.events, cutoffMs);
4114
+ bridgeQueue.absorb(history);
4115
+ if (live.length > 0)
4116
+ bridgeQueue.ingest(live, bridgeJsonlPath);
4117
+ bridgeBaselineDone = true;
4118
+ log(`Bridge restored ${restorable.length} interrupted turn(s) from journal (drain ${fromOffset}→${bridgeOffset}, absorbed=${history.length}, live=${live.length})`);
4119
+ return true;
4120
+ }
3938
4121
  /** Record `bridgeStalePidStateSessionId` if the pid file's current sid
3939
4122
  * disagrees with the just-accepted candidate's sid. Stops the next pid
3940
4123
  * resolver tick from pulling the watcher back to the stale spawn-time
@@ -4501,6 +4684,7 @@ function bridgeIngest() {
4501
4684
  for (const t of expired) {
4502
4685
  if (t.contentFingerprint)
4503
4686
  bridgeFingerprintScanLastMs.delete(t.contentFingerprint);
4687
+ journalBridgeTurnClear(t.turnId, t.dispatchAttempt);
4504
4688
  log(`Bridge mark expired after ${Math.round(BRIDGE_PENDING_TURN_TTL_MS / 1000)}s without matching a jsonl user line (turnId=${t.turnId}) — dropped to prevent rotation-fallback scan loop.`);
4505
4689
  }
4506
4690
  // Drain secondary paths first so any trailing assistant text on an old
@@ -4602,7 +4786,7 @@ function maybeEmitStructuredRateLimit(events) {
4602
4786
  // Prefer a clock parsed from the record's own text ("... resets 10:40pm");
4603
4787
  // fall back to the shared bucketed cooldown when it carries none.
4604
4788
  const usageLimit = structuredRateLimitState(apiErrorMessageText(ev));
4605
- usageLimitTracker.noteStructuredLimit();
4789
+ usageLimitTracker.noteStructuredLimit(usageLimit);
4606
4790
  send({
4607
4791
  type: 'screen_update',
4608
4792
  content: currentUsageLimitSnapshot(),
@@ -4755,6 +4939,12 @@ function stopBridgeWatcher() {
4755
4939
  // A fresh backend must not inherit a terminal/fingerprint from the exited
4756
4940
  // process; durable replay is owned by the receiver with a new attempt.
4757
4941
  bridgeQueue.clearPending();
4942
+ // A watcher teardown means this worker process has now had at least one CLI
4943
+ // generation, so any FUTURE baseline is an in-worker restart — never the
4944
+ // first-baseline cross-process recovery the journal is for. Disarm journal
4945
+ // restore: the restart's InflightInputTracker carryover already re-delivers
4946
+ // the interrupted turn, and restoring on top would double-deliver.
4947
+ bridgeRestoreGate.markGenerationRetired();
4758
4948
  bridgePreambleSent = false;
4759
4949
  }
4760
4950
  /**
@@ -4816,7 +5006,30 @@ function bridgeMarkPendingTurn(messageText, preferredTurnId, dispatchAttempt) {
4816
5006
  const normalised = normaliseForFingerprint(messageText);
4817
5007
  const contentNormalized = normalised.length > 0 ? normalised : undefined;
4818
5008
  const turnId = preferredTurnId ?? randomBytes(8).toString('hex');
4819
- bridgeQueue.mark(turnId, fingerprint, Date.now(), contentNormalized, dispatchAttempt);
5009
+ const markTimeMs = Date.now();
5010
+ bridgeQueue.mark(turnId, fingerprint, markTimeMs, contentNormalized, dispatchAttempt);
5011
+ // Journal the mark so a worker/daemon restart mid-turn can restore it
5012
+ // (bridgeAbsorbBaseline). Excluded:
5013
+ // - adopt mode: its baseline absorbs the whole transcript with different
5014
+ // semantics and never suppresses — restore marks would double-attribute;
5015
+ // - durable turns (dispatchAttempt !== undefined): a durable delivery has
5016
+ // its OWN cross-restart recovery owner (the daemon's persisted
5017
+ // queuedActivation* write-ahead + receipt auto-redispatch), so a journal
5018
+ // restore is always redundant for it and — because the recovered partial
5019
+ // and the re-dispatched attempt carry the same turnId but different
5020
+ // lastUuid — would double-deliver. Only PLAIN Lark IM turns (no other
5021
+ // recovery owner) actually need the journal.
5022
+ if (!lastInitConfig?.adoptMode && bridgeJsonlPath && dispatchAttempt === undefined) {
5023
+ journalBridgeTurnMark({
5024
+ turnId,
5025
+ dispatchAttempt,
5026
+ markTimeMs,
5027
+ fingerprint,
5028
+ contentNormalized,
5029
+ jsonlPath: bridgeJsonlPath,
5030
+ offsetAtMark: bridgeOffset,
5031
+ });
5032
+ }
4820
5033
  return turnId;
4821
5034
  }
4822
5035
  function bridgeDrainAndMaybeEmit() {
@@ -4846,6 +5059,14 @@ function emitReadyTurns(opts = {}) {
4846
5059
  });
4847
5060
  if (ready.length === 0)
4848
5061
  return;
5062
+ // Every popped turn is terminal (emitted or suppressed below) — retire its
5063
+ // durable journal entry NOW, before the final_output IPC. Clear-before-send:
5064
+ // if the worker dies in between, the answer is lost rather than duplicated
5065
+ // (the daemon-side dedupe key does not survive restarts).
5066
+ for (const turn of ready) {
5067
+ if (!turn.isLocal)
5068
+ journalBridgeTurnClear(turn.turnId, turn.dispatchAttempt);
5069
+ }
4849
5070
  const adoptMode = lastInitConfig?.adoptMode === true;
4850
5071
  // Send markers (`botmux send` landed in own thread) + the queue's first
4851
5072
  // still-unready turn. The latter caps the LAST ready turn's window —
@@ -4931,6 +5152,11 @@ function emitReadyTurns(opts = {}) {
4931
5152
  const fields = formatLocalTurnFields(rawUserText, postText);
4932
5153
  if (!fields)
4933
5154
  continue;
5155
+ // A harvested answer proves the CLI recovered from any structured
5156
+ // limit that parked it — mirror the daemon's final_output self-heal
5157
+ // (worker-pool clears ds.usageLimit there) by dropping the tracker's
5158
+ // re-emit latch so the next classify() does not re-pin the card.
5159
+ usageLimitTracker.noteTurnCompleted();
4934
5160
  send({
4935
5161
  type: 'final_output',
4936
5162
  content: fields.content,
@@ -4946,6 +5172,7 @@ function emitReadyTurns(opts = {}) {
4946
5172
  const headlessContent = formatHeadlessLocalTurnContent(postText);
4947
5173
  if (!headlessContent)
4948
5174
  continue;
5175
+ usageLimitTracker.noteTurnCompleted();
4949
5176
  send({
4950
5177
  type: 'final_output',
4951
5178
  content: headlessContent,
@@ -4956,9 +5183,17 @@ function emitReadyTurns(opts = {}) {
4956
5183
  });
4957
5184
  continue;
4958
5185
  }
5186
+ // A restored turn's fallback is a RECOVERED partial/final produced around
5187
+ // a worker restart — label it so the user can tell it from a live answer.
5188
+ // Prefix is applied AFTER the suppress gate so dedup compares the model's
5189
+ // own text, never the notice.
5190
+ const deliveredText = turn.restoredFromJournal
5191
+ ? `${t('worker.bridge_restored_turn_notice')}\n\n${postText}`
5192
+ : postText;
5193
+ usageLimitTracker.noteTurnCompleted();
4959
5194
  send({
4960
5195
  type: 'final_output',
4961
- content: postText,
5196
+ content: deliveredText,
4962
5197
  lastUuid,
4963
5198
  turnId: turn.turnId,
4964
5199
  ...(turn.dispatchAttempt !== undefined ? { dispatchAttempt: turn.dispatchAttempt } : {}),
@@ -5001,6 +5236,19 @@ function codexBridgeFallbackActive() {
5001
5236
  // services/structured-bridge-clis.ts (single source of the allowlist).
5002
5237
  return isStructuredBridgeFallbackActive(lastInitConfig?.cliId, lastInitConfig?.adoptMode === true);
5003
5238
  }
5239
+ /** A Codex App shared-adopt starts a SECOND official `codex --remote` client
5240
+ * against an external App Server/thread. It has the same transcript shape as
5241
+ * terminal `/adopt`, but is deliberately NOT `adoptMode`: BotMux still owns
5242
+ * the remote TUI and its normal Lark-input reliability rules. */
5243
+ function isExistingAppServerSharedBridge() {
5244
+ return !!lastInitConfig?.existingAppServerEndpoint;
5245
+ }
5246
+ /** `/adopt`'s split-live attach is also required for the App Server share:
5247
+ * existing history is absorbed while new, App-side turns after the attach
5248
+ * boundary can be synthesised back into the Lark conversation. */
5249
+ function codexBridgeUsesSplitLiveAttach() {
5250
+ return lastInitConfig?.adoptMode === true || isExistingAppServerSharedBridge();
5251
+ }
5004
5252
  /** Only drivers with a complete normal-final + interrupted-terminal contract
5005
5253
  * may let a transcript-started turn override the screen-ready heuristic. */
5006
5254
  function hasStructuredLifecycleBlock() {
@@ -5033,6 +5281,9 @@ function structuredBridgeIsMtr() {
5033
5281
  function structuredBridgeIsPi() {
5034
5282
  return lastInitConfig?.cliId === 'pi';
5035
5283
  }
5284
+ function structuredBridgeIsOmp() {
5285
+ return lastInitConfig?.cliId === 'oh-my-pi';
5286
+ }
5036
5287
  function structuredBridgeIsGrok() {
5037
5288
  return lastInitConfig?.cliId === 'grok';
5038
5289
  }
@@ -5042,7 +5293,7 @@ function codexBridgeIsCursor() {
5042
5293
  function currentHermesBridgeDbPath() {
5043
5294
  return hermesBridgeDbPath ?? resolveHermesStateDbPath();
5044
5295
  }
5045
- function structuredBridgeIngestPath(path, offset) {
5296
+ function structuredBridgeIngestPath(path, offset, opts = {}) {
5046
5297
  if (structuredBridgeIsCodex())
5047
5298
  return drainCodexRollout(path, offset);
5048
5299
  // adoptMode gates the drainer's bare-sentinel synthesis: adopt posts
@@ -5054,6 +5305,13 @@ function structuredBridgeIngestPath(path, offset) {
5054
5305
  return drainCursorTranscript(path, offset);
5055
5306
  if (structuredBridgeIsPi())
5056
5307
  return drainPiTranscript(path, offset);
5308
+ if (structuredBridgeIsOmp()) {
5309
+ const result = drainOmpTranscript(path, offset, ompBridgeState, {
5310
+ flushTrailingFinal: opts.flushOmpTrailingFinal,
5311
+ });
5312
+ ompBridgeState = result.state;
5313
+ return result;
5314
+ }
5057
5315
  if (structuredBridgeIsGrok())
5058
5316
  return drainGrokUpdates(path, offset);
5059
5317
  if (structuredBridgeIsHermes()) {
@@ -5145,12 +5403,14 @@ function codexBridgeStartTimer() {
5145
5403
  // fd is process-scoped, so following that fd cannot select a sibling
5146
5404
  // TRAE process merely because it shares the working directory.
5147
5405
  maybeFollowTraexSessionRotationViaPid();
5406
+ maybeFollowOmpTranscriptRotation();
5148
5407
  if (!codexBridgeRolloutPath) {
5149
5408
  // Late-attach: cliSessionId (writeInput / daemon probe) then adopt
5150
5409
  // pid. Path lookup is centralized in resolveFileBridgePath so
5151
5410
  // adding a file-backed CLI does not grow this if-tree.
5152
- // Adopt split-live; non-adopt fresh-empty (queue markTimeMs
5153
- // lower bound handles history without a split).
5411
+ // Terminal `/adopt` and an existing-App-Server share both need
5412
+ // split-live: absorb history before the attach boundary while keeping
5413
+ // new local/App-side turns after it available for forwarding.
5154
5414
  const path = resolveFileBridgePath(lastInitConfig?.cliId, {
5155
5415
  sessionId: codexBridgePendingSessionId,
5156
5416
  cwd: lastInitConfig?.workingDir,
@@ -5179,11 +5439,12 @@ function codexBridgeStartTimer() {
5179
5439
  }
5180
5440
  codexBridgePendingSessionId = undefined;
5181
5441
  codexAdoptPendingPid = undefined;
5182
- const mode = lastInitConfig?.adoptMode ? 'split-live' : 'fresh-empty';
5442
+ const mode = codexBridgeUsesSplitLiveAttach() ? 'split-live' : 'fresh-empty';
5183
5443
  codexBridgeAttach(path, mode);
5184
5444
  }
5185
5445
  }
5186
5446
  codexBridgeIngest();
5447
+ maybeFlushOmpTrailingFinalOnQuietTick();
5187
5448
  if (isPromptReady)
5188
5449
  emitReadyCodexTurns();
5189
5450
  }
@@ -5285,6 +5546,9 @@ function mtrBridgeIngest() {
5285
5546
  }
5286
5547
  }
5287
5548
  function codexBridgeAttach(rolloutPath, mode) {
5549
+ ompBridgeState = {};
5550
+ ompQuietCandidateKey = undefined;
5551
+ ompQuietCandidateCompleteOffset = undefined;
5288
5552
  codexBridgeRolloutPath = rolloutPath;
5289
5553
  if (structuredBridgeIsCodex())
5290
5554
  codexServiceTierTracker.bind(rolloutPath);
@@ -5468,6 +5732,9 @@ function codexBridgeDetachFile() {
5468
5732
  codexBridgeOffset = 0;
5469
5733
  codexBridgePendingTail = '';
5470
5734
  codexBridgeBaselineDone = false;
5735
+ ompBridgeState = {};
5736
+ ompQuietCandidateKey = undefined;
5737
+ ompQuietCandidateCompleteOffset = undefined;
5471
5738
  }
5472
5739
  /** Resolve the pid of the Codex process this worker observes (spawned child or
5473
5740
  * adopted pane), mirroring the grok/traex pid-follow resolution order. */
@@ -5588,7 +5855,7 @@ function codexBridgeNotifyCliSessionId(cliSessionId) {
5588
5855
  const pid = currentCodexObservedPid();
5589
5856
  const next = resolveFileBridgePath('codex', { sessionId: cliSessionId });
5590
5857
  if (next && next !== codexBridgeRolloutPath) {
5591
- const attachMode = lastInitConfig?.adoptMode ? 'split-live' : 'fresh-empty';
5858
+ const attachMode = codexBridgeUsesSplitLiveAttach() ? 'split-live' : 'fresh-empty';
5592
5859
  log(`Codex session binding corrected ${currentSid ?? '?'} → ${cliSessionId} (pid ${pid} owns it); re-attaching bridge to ${next}`);
5593
5860
  codexBridgeDetachFile();
5594
5861
  codexBridgePendingSessionId = undefined;
@@ -5717,7 +5984,7 @@ function codexBridgeNotifyCliSessionId(cliSessionId) {
5717
5984
  }
5718
5985
  return;
5719
5986
  }
5720
- // Codex INITIAL attach (no prior rollout bound). The multi-fd adopt case
5987
+ // Codex INITIAL attach (no prior rollout bound). The multi-fd terminal-adopt case
5721
5988
  // reaches here with codexBridgeRolloutPath still unset: findCodexRolloutByPid
5722
5989
  // returned undefined (ambiguous parent+sibling), so the adopt block armed the
5723
5990
  // poller instead of attaching. The cliSessionId is normally already
@@ -5755,7 +6022,7 @@ function codexBridgeNotifyCliSessionId(cliSessionId) {
5755
6022
  });
5756
6023
  if (path) {
5757
6024
  codexBridgePendingSessionId = undefined;
5758
- codexBridgeAttach(path, 'fresh-empty');
6025
+ codexBridgeAttach(path, codexBridgeUsesSplitLiveAttach() ? 'split-live' : 'fresh-empty');
5759
6026
  }
5760
6027
  else {
5761
6028
  codexBridgePendingSessionId = cliSessionId;
@@ -5807,6 +6074,30 @@ function maybeFollowTraexSessionRotationViaPid() {
5807
6074
  persistCliSessionId(observed.cliSessionId);
5808
6075
  codexBridgeNotifyCliSessionId(observed.cliSessionId);
5809
6076
  }
6077
+ /** OMP may create a new JSONL in the same isolated Botmux session directory.
6078
+ * Follow only that exact directory; no pid/global-session discovery is used. */
6079
+ function maybeFollowOmpTranscriptRotation() {
6080
+ if (!structuredBridgeIsOmp() || !codexBridgeRolloutPath
6081
+ || !lastSpawnEffectiveAdapterSessionId)
6082
+ return;
6083
+ const next = resolveFileBridgePath('oh-my-pi', {
6084
+ sessionId: lastSpawnEffectiveAdapterSessionId,
6085
+ });
6086
+ if (!next || next === codexBridgeRolloutPath || ompRetiredTranscriptPaths.has(next))
6087
+ return;
6088
+ const retired = codexBridgeRolloutPath;
6089
+ try {
6090
+ codexBridgeIngest();
6091
+ emitReadyCodexTurns();
6092
+ }
6093
+ catch (err) {
6094
+ log(`OMP pre-rotation bridge drain failed: ${err.message}`);
6095
+ }
6096
+ log(`OMP transcript rotated: ${codexBridgeRolloutPath} → ${next}`);
6097
+ ompRetiredTranscriptPaths.add(retired);
6098
+ codexBridgeDetachFile();
6099
+ codexBridgeAttach(next, 'fresh-empty');
6100
+ }
5810
6101
  function codexBridgeIngest(opts = {}) {
5811
6102
  // Follow-up RPC turns install their exact bridge mark only after the
5812
6103
  // turn/start ACK passes the generation fence. Ordinary ingest must not
@@ -5825,7 +6116,9 @@ function codexBridgeIngest(opts = {}) {
5825
6116
  }
5826
6117
  if (!codexBridgeRolloutPath || !codexBridgeBaselineDone)
5827
6118
  return;
5828
- const result = structuredBridgeIngestPath(codexBridgeRolloutPath, codexBridgeOffset);
6119
+ const result = structuredBridgeIngestPath(codexBridgeRolloutPath, codexBridgeOffset, {
6120
+ flushOmpTrailingFinal: opts.flushOmpTrailingFinal,
6121
+ });
5829
6122
  codexBridgeOffset = result.newOffset;
5830
6123
  codexBridgePendingTail = result.pendingTail;
5831
6124
  if (structuredBridgeIsTraex()) {
@@ -5861,6 +6154,42 @@ function codexBridgeIngest(opts = {}) {
5861
6154
  idleDetector?.fireIdle();
5862
6155
  }
5863
6156
  }
6157
+ /** Confirm an OMP file-tail terminal only after one complete quiet bridge tick
6158
+ * and an authoritative current viewport with no busy marker. This deliberately
6159
+ * runs while lifecycle blocking is asserted; ingesting the final releases it. */
6160
+ function maybeFlushOmpTrailingFinalOnQuietTick() {
6161
+ if (!structuredBridgeIsOmp())
6162
+ return;
6163
+ const candidate = ompBridgeState.provisionalFinal;
6164
+ if (!candidate) {
6165
+ ompQuietCandidateKey = undefined;
6166
+ ompQuietCandidateCompleteOffset = undefined;
6167
+ return;
6168
+ }
6169
+ const key = candidate.event.uuid;
6170
+ if (ompQuietCandidateKey !== key
6171
+ || ompQuietCandidateCompleteOffset !== codexBridgeOffset) {
6172
+ ompQuietCandidateKey = key;
6173
+ ompQuietCandidateCompleteOffset = codexBridgeOffset;
6174
+ return;
6175
+ }
6176
+ if (codexBridgePendingTail || !hasStructuredLifecycleBlock() || !backend
6177
+ || !backendScreenEvidenceIsAuthoritativeForMutation()
6178
+ || !cliAdapter?.busyPattern)
6179
+ return;
6180
+ try {
6181
+ const screen = captureBackendScreen(backend);
6182
+ if (!screen || cliAdapter.busyPattern.test(busyProbeRegion(screen)))
6183
+ return;
6184
+ }
6185
+ catch (err) {
6186
+ log(`OMP quiet-final viewport capture failed: ${err.message}`);
6187
+ return;
6188
+ }
6189
+ ompQuietCandidateKey = undefined;
6190
+ ompQuietCandidateCompleteOffset = undefined;
6191
+ codexBridgeIngest({ flushOmpTrailingFinal: true });
6192
+ }
5864
6193
  /** 将 Codex 的结构化 429 终态同步为既有的限流状态。 */
5865
6194
  function maybeEmitCodexStructuredRateLimit(events) {
5866
6195
  for (const ev of events) {
@@ -5868,7 +6197,7 @@ function maybeEmitCodexStructuredRateLimit(events) {
5868
6197
  continue;
5869
6198
  emittedRateLimitUuids.add(ev.uuid);
5870
6199
  const usageLimit = structuredRateLimitState(ev.terminalErrorSummary ?? '429 Too Many Requests');
5871
- usageLimitTracker.noteStructuredLimit();
6200
+ usageLimitTracker.noteStructuredLimit(usageLimit);
5872
6201
  send({
5873
6202
  type: 'screen_update',
5874
6203
  content: currentUsageLimitSnapshot(),
@@ -6245,11 +6574,12 @@ function emitReadyCodexTurns() {
6245
6574
  // that flag to settle completed-with-empty; a bare `completed` terminal (e.g.
6246
6575
  // the RPC-hydration timeout path) must never be read as silence.
6247
6576
  const nothingToSendTurns = new Set();
6248
- const adoptMode = lastInitConfig?.adoptMode === true;
6577
+ const terminalAdoptMode = lastInitConfig?.adoptMode === true;
6578
+ const sharedAppServerBridge = isExistingAppServerSharedBridge();
6249
6579
  // Adopt mode: model is the user's external Codex, no botmux send to
6250
6580
  // gate against — every assistant turn (Lark-driven OR locally typed)
6251
6581
  // should reach the thread. Skip marker IO entirely.
6252
- const markers = adoptMode ? [] : readSendMarkers();
6582
+ const markers = terminalAdoptMode ? [] : readSendMarkers();
6253
6583
  const remaining = codexBridgeQueue.peek();
6254
6584
  // Only a STARTED pending turn can bound the last ready turn's send window.
6255
6585
  // An unstarted turn hasn't been dequeued yet (its user event hasn't landed),
@@ -6264,6 +6594,13 @@ function emitReadyCodexTurns() {
6264
6594
  : undefined;
6265
6595
  for (let i = 0; i < ready.length; i++) {
6266
6596
  const turn = ready[i];
6597
+ // A shared App Server session still owns the BotMux remote TUI, so its
6598
+ // Lark-originated turns retain normal send-marker deduplication. Only a
6599
+ // turn synthesized from Codex App input is external/local: that side has
6600
+ // no pending Lark fingerprint and must be forwarded like terminal
6601
+ // `/adopt`, including both the App prompt and its final reply.
6602
+ const adoptMode = terminalAdoptMode
6603
+ || (sharedAppServerBridge && turn.isLocal === true);
6267
6604
  const sourceHermesSessionId = structuredBridgeIsHermes() ? turn.sourceSessionId : undefined;
6268
6605
  const nextBoundaryMs = (i + 1 < ready.length ? ready[i + 1].markTimeMs : nextPendingMarkTimeMs);
6269
6606
  const gateInput = {
@@ -6319,6 +6656,9 @@ function emitReadyCodexTurns() {
6319
6656
  const fields = formatLocalTurnFields(turn.userText ?? '', postContent);
6320
6657
  if (!fields)
6321
6658
  continue;
6659
+ // Harvested answer → drop the structured-limit re-emit latch (mirrors
6660
+ // the daemon's final_output self-heal; see emitReadyTurns).
6661
+ usageLimitTracker.noteTurnCompleted();
6322
6662
  send({
6323
6663
  type: 'final_output',
6324
6664
  ...(sourceHermesSessionId ? { sourceHermesSessionId } : {}),
@@ -6331,6 +6671,7 @@ function emitReadyCodexTurns() {
6331
6671
  });
6332
6672
  continue;
6333
6673
  }
6674
+ usageLimitTracker.noteTurnCompleted();
6334
6675
  send({
6335
6676
  type: 'final_output',
6336
6677
  ...(sourceHermesSessionId ? { sourceHermesSessionId } : {}),
@@ -6369,6 +6710,10 @@ function stopCodexBridge() {
6369
6710
  codexBridgeOffset = 0;
6370
6711
  codexBridgePendingTail = '';
6371
6712
  codexBridgeBaselineDone = false;
6713
+ ompBridgeState = {};
6714
+ ompQuietCandidateKey = undefined;
6715
+ ompQuietCandidateCompleteOffset = undefined;
6716
+ ompRetiredTranscriptPaths.clear();
6372
6717
  hermesBridgeOffset = 0;
6373
6718
  hermesBridgeBaselineDone = false;
6374
6719
  hermesBridgeSourceSessionId = undefined;
@@ -7803,6 +8148,13 @@ function handleVisibleStartupInteraction(data) {
7803
8148
  // row; never let its selection marker reach the first-prompt idle detector.
7804
8149
  if (dismissAidenCodexUpdateDialog(data))
7805
8150
  return true;
8151
+ // An existing App Server viewer shares the user's Codex App trust context.
8152
+ // Never auto-accept an interactive trust prompt here: it can include
8153
+ // project/plugin hooks owned outside BotMux. The operator can safely choose
8154
+ // "Continue without trusting" through the Web Terminal when Codex presents
8155
+ // its Hooks review menu.
8156
+ if (lastInitConfig?.existingAppServerEndpoint)
8157
+ return false;
7806
8158
  if (trustHandled)
7807
8159
  return false;
7808
8160
  const stripped = data.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
@@ -8612,6 +8964,7 @@ function onPtyData(data) {
8612
8964
  }
8613
8965
  }
8614
8966
  renderer?.write(data);
8967
+ refreshHookReviewInputHold(`${renderer?.rawSnapshot() ?? ''}\n${data}`);
8615
8968
  // In tmux-attach mode, each web client has its own tmux attach PTY —
8616
8969
  // no relay needed. In non-tmux mode AND in pipe mode (adopt-bridge),
8617
8970
  // broadcast through the shared scrollback so all connected web clients
@@ -8721,6 +9074,7 @@ async function onBackendScreenResync(snapshot) {
8721
9074
  // current viewport: a prior local attach may have changed the real geometry.
8722
9075
  const visibleSnapshot = nextRenderer?.rawSnapshot() ?? '';
8723
9076
  lastAnalyzerSnapshot = visibleSnapshot;
9077
+ refreshHookReviewInputHold(visibleSnapshot);
8724
9078
  // ZMX history does not carry the authoritative current PTY dimensions. A
8725
9079
  // local `zmx attach` can resize the session below our default 120x24 and that
8726
9080
  // size persists after detach, so even the rendered tail may include rows just
@@ -9329,6 +9683,7 @@ function dropFailedBridgeMark(bridgeTurnId, dispatchAttempt) {
9329
9683
  if (dropped) {
9330
9684
  if (dropped.contentFingerprint)
9331
9685
  bridgeFingerprintScanLastMs.delete(dropped.contentFingerprint);
9686
+ journalBridgeTurnClear(bridgeTurnId, dispatchAttempt);
9332
9687
  log(`Bridge mark dropped after submit failure (turnId=${bridgeTurnId}) — rotation-fallback scan will stop spinning on this fingerprint.`);
9333
9688
  }
9334
9689
  if (droppedStructured) {
@@ -9429,12 +9784,9 @@ function scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId
9429
9784
  case 'suppress-active':
9430
9785
  redriveRejectedStructuredReady();
9431
9786
  log(`Deferred recheck missing but later ${action.evidence} shows ${cliName()} is active — suppressing submit warning. preview="${preview}"`);
9432
- // For a durable turn, activity is evidence of possible submission, not
9433
- // a terminal result. Keep the arbiter closed and re-check until either
9434
- // the transcript confirms it or a quiet interval proves it stuck.
9435
- if (turnIdentity?.dispatchAttempt !== undefined) {
9436
- scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId, undefined, turnSeq, turnIdentity, durableTerminalStatus);
9437
- }
9787
+ // activity 证据只能说明 CLI 仍在动,不能证明本次输入已提交
9788
+ // 普通 IM durable turn 都继续重查,直到 transcript 命中或安静窗口落到 submit_unconfirmed
9789
+ scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId, undefined, turnSeq, turnIdentity, durableTerminalStatus, structuredTarget);
9438
9790
  return;
9439
9791
  case 'notify-hard-failure':
9440
9792
  // failureReason is handled synchronously above.
@@ -9608,7 +9960,7 @@ async function flushPending() {
9608
9960
  }
9609
9961
  if (ambiguousSubmissionRecoveryHold?.backend === backend)
9610
9962
  return;
9611
- if (pendingMessages.length === 0 && pendingAdoptMessages.length === 0 && pendingRawInputs.length === 0 && pendingSessionRename === null)
9963
+ if (!hasPendingInputForFlush())
9612
9964
  return; // nothing to flush — keep isPromptReady
9613
9965
  if (sessionRenameInFlight())
9614
9966
  return; // wait for /rename to finish before any user input
@@ -9627,6 +9979,14 @@ async function flushPending() {
9627
9979
  return;
9628
9980
  if (bareShellLaunchBlocked)
9629
9981
  return; // launch is held at a bare shell — don't type prompts into it
9982
+ // A Hook-review page has an active selection and consumes Enter/arrow keys.
9983
+ // Re-check the rendered viewport right before a literal write; PTY chunks
9984
+ // can arrive between idle detection and this queue drain.
9985
+ refreshHookReviewInputHold(lastAnalyzerSnapshot || renderer?.rawSnapshot() || '');
9986
+ if (hookReviewInputHold) {
9987
+ notifyHookReviewInputHold();
9988
+ return;
9989
+ }
9630
9990
  // Screen-idle is not a durable receipt. A permission/AskUser prompt can look
9631
9991
  // idle while the logical delivery is unresolved, so no following IM or
9632
9992
  // meeting turn may cross this boundary until an explicit terminal releases
@@ -10952,6 +11312,27 @@ function scheduleBusyPatternIdleProbe(source) {
10952
11312
  }
10953
11313
  async function spawnCli(cfg, opts = {}) {
10954
11314
  const spawnGeneration = ++cliSpawnGeneration;
11315
+ // Experimental external App Server attachment: BotMux owns only this TUI
11316
+ // client, never the server or its JSON-RPC input stream. Re-establish the
11317
+ // remote argv state on EVERY spawn because killCli() deliberately clears the
11318
+ // generic RPC remote variables before a restart.
11319
+ if (cfg.existingAppServerEndpoint !== undefined) {
11320
+ if (cfg.cliId !== 'codex') {
11321
+ throw new Error('existing App Server attachment requires cliId "codex"');
11322
+ }
11323
+ if (!cfg.cliSessionId) {
11324
+ throw new Error('existing App Server attachment requires an already-selected Codex thread id');
11325
+ }
11326
+ if (cfg.codexRpcInput === true) {
11327
+ throw new Error('existing App Server attachment cannot enable BotMux codexRpcInput');
11328
+ }
11329
+ if (codexRpcEngine) {
11330
+ throw new Error('existing App Server attachment cannot share a BotMux-owned Codex RPC engine');
11331
+ }
11332
+ remoteWsUrl = normalizeExistingAppServerEndpoint(cfg.existingAppServerEndpoint, `worker existingAppServerEndpoint for session ${cfg.sessionId}`);
11333
+ remoteThreadId = cfg.cliSessionId;
11334
+ log(`External Codex App Server attach prepared for thread ${cfg.cliSessionId.substring(0, 12)}`);
11335
+ }
10955
11336
  // Prefer force-clear so a half-finished rename cannot block the new generation.
10956
11337
  forceClearSessionRenameInFlight();
10957
11338
  currentCliCredentialIsolated = false;
@@ -11121,7 +11502,16 @@ async function spawnCli(cfg, opts = {}) {
11121
11502
  log(`Adopt mode (${effectiveBackendType}): observing ${target} (${cols}x${rows})`);
11122
11503
  return;
11123
11504
  }
11124
- cliAdapter = createCliAdapterSync(cfg.cliId, cfg.cliPathOverride);
11505
+ // dsh runtime variant: when the bot selected dsh-tui (dshRuntime='tui'),
11506
+ // resolve the PTY-driven dsh-tui adapter instead of the headless JSON-RPC
11507
+ // runner. The bot's cliId stays 'dsh' (the toggle is a per-bot config field),
11508
+ // so every other dsh-specific branch (OSC decode, turn timeout, etc.) is
11509
+ // unaffected — dsh-tui simply doesn't emit OSC frames, making the decoder a
11510
+ // no-op for it.
11511
+ const effectiveCliId = cfg.cliId === 'dsh' && cfg.dshRuntime === 'tui'
11512
+ ? 'dsh-tui'
11513
+ : cfg.cliId;
11514
+ cliAdapter = createCliAdapterSync(effectiveCliId, cfg.cliPathOverride);
11125
11515
  // backendType trust-but-verify + HARD GATE (PTY 退役): an explicit per-bot
11126
11516
  // config (or BACKEND_TYPE env override) bypasses config.ts's default, so the
11127
11517
  // worker re-probes the requested persistent backend here. A requested
@@ -11927,11 +12317,35 @@ async function spawnCli(cfg, opts = {}) {
11927
12317
  if (effectiveBackendType === 'zmx') {
11928
12318
  resolvedZmxSessionProbe = paneProbe;
11929
12319
  }
11930
- if (paneProbe === 'exists') {
11931
- // The trusted Gateway host belongs to the worker and cannot survive a
11932
- // worker/daemon replacement. Cold-resume the CLI so its MCP client gets a
11933
- // fresh relay socket instead of reattaching to a dead connection.
11934
- log(`[mcp-gateway] persistent pane ${cfg.sessionId} has plugin MCP state cold-resuming with a fresh host`);
12320
+ const paneRelayReattachSafe = paneProbe === 'exists'
12321
+ && mcpGatewayPaneReattachSafe(readMcpGatewayLaunchRecord(config.session.dataDir, cfg.sessionId), sessionMcpGatewaySocketPath(cfg.sessionId, config.session.dataDir));
12322
+ if (paneRelayReattachSafe) {
12323
+ // The pane's CLI was launched against the deterministic Gateway socket
12324
+ // path by a reconnect-capable relay. Reattaching preserves whatever turn
12325
+ // the CLI is executing — the whole point of the persistent backend — but
12326
+ // the trusted host died with the previous worker, so we must RE-SERVE it
12327
+ // here at the same deterministic path for the pane's surviving relay to
12328
+ // reconnect to. This reattach path skips prepareCliPluginGenerationAndGateway
12329
+ // (it must not refresh the catalog on a warm reattach), which is the only
12330
+ // other host starter; without this the relay would reconnect forever to a
12331
+ // socket nothing binds. Start only when this fresh worker has no live host
12332
+ // yet — never stomp a host an in-worker restart already brought up.
12333
+ if (!sessionMcpGatewayHost) {
12334
+ await startAndRecordSessionMcpGatewayHost(cfg.sessionId);
12335
+ if (spawnGeneration !== cliSpawnGeneration)
12336
+ throw new CliSpawnSupersededError();
12337
+ log(`[mcp-gateway] persistent pane ${cfg.sessionId} keeps its relay socket path — re-served trusted host at the same path; relay reconnects on its own`);
12338
+ }
12339
+ else {
12340
+ log(`[mcp-gateway] persistent pane ${cfg.sessionId} keeps its relay socket path — reattaching; live host already bound, relay stays connected`);
12341
+ }
12342
+ }
12343
+ else if (paneProbe === 'exists') {
12344
+ // Legacy pane (mkdtemp-random socket path, or a relay predating
12345
+ // reconnect support): its MCP client can never reach the replacement
12346
+ // host. Cold-resume the CLI so it gets a fresh relay socket instead of
12347
+ // reattaching to a dead connection.
12348
+ log(`[mcp-gateway] persistent pane ${cfg.sessionId} has plugin MCP state without a reconnect-capable relay — cold-resuming with a fresh host`);
11935
12349
  const persistentBackendType = effectiveBackendType;
11936
12350
  const persistentTarget = selectedBackend.persistentBackendTarget;
11937
12351
  if (effectiveBackendType === 'zmx') {
@@ -12050,6 +12464,8 @@ async function spawnCli(cfg, opts = {}) {
12050
12464
  bareShellLaunchBlocked = false;
12051
12465
  bareShellChecked = false;
12052
12466
  bareShellCheckInProgress = false;
12467
+ hookReviewInputHold = false;
12468
+ hookReviewInputHoldNotified = false;
12053
12469
  // ── Resume pre-flight check + two-tier fallback ──────────────────────────
12054
12470
  // Tier 0 (adapter capability): adapters whose buildArgs can only resume a
12055
12471
  // PRECISE cliSessionId (cursor/copilot/kimi — no --continue/latest fallback,
@@ -12171,6 +12587,8 @@ async function spawnCli(cfg, opts = {}) {
12171
12587
  // the fresh session's first turn.
12172
12588
  lastSpawnEffectiveResume = effectiveResume;
12173
12589
  lastSpawnEffectiveCliSessionId = effectiveCliSessionId;
12590
+ lastSpawnEffectiveAdapterSessionId = effectiveAdapterSessionId;
12591
+ ompRetiredTranscriptPaths.clear();
12174
12592
  // ttadk 网关:模型走 ttadk 自己的 `-m`(启动期注入到 ttadk 前缀,见下方 wrapperCli
12175
12593
  // 分支),不能再把 cfg.model 透给底层适配器,否则真实 CLI 会再吃一个 --model 重复。
12176
12594
  const ttadkGateway = isTtadkWrapper(cfg.wrapperCli);
@@ -12667,6 +13085,47 @@ async function spawnCli(cfg, opts = {}) {
12667
13085
  }
12668
13086
  return out;
12669
13087
  };
13088
+ // Canonicalize a path whose LEAF may not exist yet (e.g. a lark-cli keystore dir
13089
+ // lark-cli will create on first write): realpath the DEEPEST EXISTING ancestor
13090
+ // (resolving any symlinked prefix — symlinked $HOME, a symlinked data root) and
13091
+ // re-append the non-existent tail. A plain full-path realpath THROWS on the
13092
+ // missing leaf and returns the raw lexical value, so a symlinked ancestor would
13093
+ // stay unresolved and the policy could anchor a DIFFERENT namespace than the one
13094
+ // lark-cli opens after IT canonicalizes (a canonicalization TOCTOU). Input must already be
13095
+ // lexically clean (no `..`); resolveLarkCliLinuxStoreDir Cleans it first.
13096
+ const canonicalNearestAncestor = (p) => {
13097
+ if (!p.startsWith('/'))
13098
+ return p;
13099
+ const segs = p.split('/').filter(Boolean);
13100
+ for (let i = segs.length; i >= 0; i--) {
13101
+ const prefix = '/' + segs.slice(0, i).join('/');
13102
+ try {
13103
+ const real = realpathSync(prefix);
13104
+ return i === segs.length ? real : `${real}/${segs.slice(i).join('/')}`;
13105
+ }
13106
+ catch { /* ancestor missing → try a shallower prefix */ }
13107
+ }
13108
+ return p; // not even '/' resolved (impossible) → lexical fallback
13109
+ };
13110
+ // OMP keeps all transcripts under one shared state root. Materialize this
13111
+ // Botmux session's exact directory before allow-path existence filtering;
13112
+ // the policy below masks the shared sessions parent and re-opens only this
13113
+ // deeper directory read-write.
13114
+ const ompCurrentSessionDir = cliAdapter.id === 'oh-my-pi'
13115
+ ? ompSessionDir(effectiveAdapterSessionId)
13116
+ : undefined;
13117
+ if (ompCurrentSessionDir)
13118
+ mkdirSync(ompCurrentSessionDir, { recursive: true, mode: 0o700 });
13119
+ const canonicalOmpSessionsRoot = ompCurrentSessionDir
13120
+ ? canonical(dirname(dirname(ompCurrentSessionDir)))
13121
+ : undefined;
13122
+ const canonicalOmpSessionDir = ompCurrentSessionDir
13123
+ ? canonical(ompCurrentSessionDir)
13124
+ : undefined;
13125
+ if (canonicalOmpSessionsRoot && canonicalOmpSessionDir
13126
+ && relative(canonicalOmpSessionsRoot, canonicalOmpSessionDir) !== join('botmux', effectiveAdapterSessionId)) {
13127
+ throw new Error('[sandbox] OMP session directory escapes its managed sessions root');
13128
+ }
12670
13129
  // User three-tier lists: the new sandboxPaths field, or a pre-migration
12671
13130
  // session/config's legacy fields mapped through the SAME lossless mapping
12672
13131
  // the startup migration uses.
@@ -12786,6 +13245,9 @@ async function spawnCli(cfg, opts = {}) {
12786
13245
  if (process.platform === 'linux') {
12787
13246
  mandatoryDenyPaths.push(join(canonical(dataDir), 'sandboxes', cfg.sessionId));
12788
13247
  }
13248
+ if (canonicalOmpSessionsRoot) {
13249
+ mandatoryDenyPaths.push(canonicalOmpSessionsRoot);
13250
+ }
12789
13251
  if (process.platform === 'darwin') {
12790
13252
  const osUserHomeDir = userInfo().homedir;
12791
13253
  if (!osUserHomeDir) {
@@ -12917,6 +13379,23 @@ async function spawnCli(cfg, opts = {}) {
12917
13379
  // arbitrary parent dir (`/tmp`, `/etc`, a project root) and bricking the
12918
13380
  // core CLI (codex P1). Canonicalized so it shares the roots' namespace.
12919
13381
  loadedBotsConfigPath: cfg.loadedBotsConfigPath ? canonical(cfg.loadedBotsConfigPath) : undefined,
13382
+ // Linux lark-cli keystore (holds every bot's appsecret ciphertext + the shared
13383
+ // master key). Resolve it EXACTLY as the in-sandbox lark-cli will:
13384
+ // `<clean(LARKSUITE_CLI_DATA_DIR)>/lark-cli` when that env var is a valid ABSOLUTE
13385
+ // path (lexically Cleaned — `..`/`.`/`//` resolved, control chars rejected), else
13386
+ // `$HOME/.local/share/lark-cli` — lark-cli does NOT read XDG_DATA_HOME (verified by
13387
+ // strace on v1.0.76 + its keychain_other.go::StorageDir / SafeEnvDirPath). The
13388
+ // Clean happens INSIDE resolveLarkCliLinuxStoreDir (a `..`-bearing value
13389
+ // must be normalized here, or realpath throws on the nonexistent leaf, the raw `..`
13390
+ // survives, normalizeFsPath rejects it, and the policy anchors the DEFAULT store
13391
+ // while lark-cli opens the Clean'd real one → leak). canonicalNearestAncestor then
13392
+ // resolves any SYMLINKED ancestor even when the store leaf does not exist yet (a
13393
+ // full-path realpath would throw and skip symlink resolution → policy/CLI namespace
13394
+ // divergence). bwrap has no --clearenv, so the child inherits this bot's
13395
+ // LARKSUITE_CLI_DATA_DIR from the worker's own (frozen, non-agent-controllable)
13396
+ // process.env, and sandbox.ts pins the child to the SAME cleaned resolution
13397
+ // (--setenv/--unsetenv) so policy == CLI by construction. Ignored on darwin.
13398
+ larkCliLinuxStore: canonicalNearestAncestor(resolveLarkCliLinuxStoreDir(process.env.LARKSUITE_CLI_DATA_DIR, lexicalHome)),
12920
13399
  redirectedCliData: willRedirectCliData,
12921
13400
  cliDataPaths: willRedirectCliData ? undefined : keepExisting([
12922
13401
  cliAdapter.claudeDataDir,
@@ -12968,7 +13447,7 @@ async function spawnCli(cfg, opts = {}) {
12968
13447
  ]),
12969
13448
  botmuxInstallRoot,
12970
13449
  outbox,
12971
- extraWritePaths: keepExisting([process.env.TMPDIR]),
13450
+ extraWritePaths: keepExisting([process.env.TMPDIR, canonicalOmpSessionDir]),
12972
13451
  userPaths,
12973
13452
  mandatoryDenyPaths,
12974
13453
  mandatoryDenyRegexes,
@@ -13055,6 +13534,25 @@ async function spawnCli(cfg, opts = {}) {
13055
13534
  }
13056
13535
  }
13057
13536
  else {
13537
+ // Canonical lark-cli data root pinned into the child so the in-sandbox lark-cli
13538
+ // resolves the SAME physical keystore the policy denied/carved-out (avoiding a
13539
+ // symlinked-data-root namespace split). fsPolicyCtx.larkCliLinuxStore is the FULLY-canonical store dir (leaf
13540
+ // symlinks followed) — the physical location the policy protects. The child opens
13541
+ // `<pin>/lark-cli`, so pin = dirname(store) is same-source ONLY when the canonical
13542
+ // store's basename is still `lark-cli` (the common case, incl. a symlink that
13543
+ // resolves to another `lark-cli` dir). If the `lark-cli` leaf was a symlink to a
13544
+ // DIFFERENTLY-named dir (basename !== 'lark-cli'), dirname(store)+'/lark-cli' would
13545
+ // NOT equal the canonical store → child would open an unbound path (auth breaks) OR
13546
+ // a foreign one. There is no leak either way (the policy still denies the real dir,
13547
+ // and both candidate stores are locked), so degrade SAFELY: pin nothing → child
13548
+ // falls back to the default store (also locked), and log the exotic layout.
13549
+ const canonStore = fsPolicyCtx.larkCliLinuxStore;
13550
+ const childLarkDataRoot = larkCliChildDataRoot(canonStore);
13551
+ if (canonStore && childLarkDataRoot === null) {
13552
+ log(`[sandbox] lark-cli keystore ${canonStore} does not end in 'lark-cli' (symlinked leaf?) — `
13553
+ + `not pinning a custom LARKSUITE_CLI_DATA_DIR into the sandbox; the in-sandbox lark-cli will `
13554
+ + `use the default store. The real keystore is still denied/carve-out'd by policy (no leak).`);
13555
+ }
13058
13556
  const sbx = prepareDirectSandbox({
13059
13557
  sessionId: cfg.sessionId,
13060
13558
  dataDir,
@@ -13065,6 +13563,7 @@ async function spawnCli(cfg, opts = {}) {
13065
13563
  cliArgs: args,
13066
13564
  trustedBotmuxCommandPaths: [defaultGatewayEntry().command],
13067
13565
  mcpGatewaySocketPath: sessionMcpGatewayHost?.socketPath,
13566
+ larkCliDataDir: childLarkDataRoot,
13068
13567
  });
13069
13568
  if (!sbx) {
13070
13569
  // FAIL-SAFE: never silently run unsandboxed.
@@ -13606,6 +14105,13 @@ async function spawnCli(cfg, opts = {}) {
13606
14105
  // can verify they were spawned inside a botmux session by walking the
13607
14106
  // process tree and looking for a matching pid file in this directory.
13608
14107
  const cliPid = backend.getChildPid?.();
14108
+ if (cfg.existingAppServerEndpoint !== undefined) {
14109
+ // The local `codex --remote` client does not own the selected rollout fd;
14110
+ // writeInput must accept only the explicitly bound remote thread in the
14111
+ // shared history.jsonl instead of applying its normal local-PID ownership
14112
+ // filter (which would reject the correct App Server submission).
14113
+ backend.expectedCodexSessionId = cfg.cliSessionId;
14114
+ }
13609
14115
  publishLocalProcessAttestation(cliPid ?? undefined);
13610
14116
  if (cliPid && process.env.SESSION_DATA_DIR) {
13611
14117
  const markersDir = join(process.env.SESSION_DATA_DIR, '.botmux-cli-pids');
@@ -13809,10 +14315,21 @@ async function spawnCli(cfg, opts = {}) {
13809
14315
  hermesBridgeAttach(effectiveResume ? 'baseline-existing' : 'fresh-empty');
13810
14316
  }
13811
14317
  else if (cfg.cliId === 'codex') {
14318
+ if (cfg.existingAppServerEndpoint) {
14319
+ // A shared Codex App thread has two legitimate writers: the App and
14320
+ // BotMux's official `codex --remote` client. Enable local-turn
14321
+ // synthesis only from this share's attach boundary, so older thread
14322
+ // history is absorbed while future App-side turns are mirrored to Lark.
14323
+ codexAdoptStartMs = Date.now();
14324
+ codexBridgeQueue.setLocalTurns(true, codexAdoptStartMs);
14325
+ log('Codex bridge enabled App Server shared-turn forwarding');
14326
+ }
13812
14327
  if (effectiveCliSessionId) {
13813
14328
  const rolloutPath = findCodexRolloutBySessionId(effectiveCliSessionId);
13814
14329
  if (rolloutPath) {
13815
- codexBridgeAttach(rolloutPath, effectiveResume ? 'baseline-existing' : 'fresh-empty');
14330
+ codexBridgeAttach(rolloutPath, cfg.existingAppServerEndpoint
14331
+ ? 'split-live'
14332
+ : effectiveResume ? 'baseline-existing' : 'fresh-empty');
13816
14333
  }
13817
14334
  else {
13818
14335
  codexBridgePendingSessionId = effectiveCliSessionId;
@@ -13858,7 +14375,7 @@ async function spawnCli(cfg, opts = {}) {
13858
14375
  codexBridgeStartTimer();
13859
14376
  }
13860
14377
  }
13861
- else if (cfg.cliId === 'pi' || cfg.cliId === 'grok') {
14378
+ else if (cfg.cliId === 'pi' || cfg.cliId === 'grok' || cfg.cliId === 'oh-my-pi') {
13862
14379
  // File-backed: pin path when known (pi session id / grok --session-id
13863
14380
  // UUID), else arm the poller. Grok collision-fallback (dir already
13864
14381
  // exists → no --session-id → grok mints id) is recovered via writeInput
@@ -14012,7 +14529,8 @@ async function spawnCli(cfg, opts = {}) {
14012
14529
  // itself fail-opens on non-authoritative screens (ZMX), so a stale
14013
14530
  // `Working...` in ZMX history can never block a structured terminal.
14014
14531
  const busyGuardedIdle = evidenceSource === 'screen'
14015
- || (evidenceSource === 'external' && structuredBridgeIsPi());
14532
+ || (evidenceSource === 'external'
14533
+ && (structuredBridgeIsPi() || structuredBridgeIsOmp()));
14016
14534
  if (busyGuardedIdle && idleBackend
14017
14535
  && deferPromptReadyWhileBusy(`${cliName()} ${evidenceSource}-idle`, idleBackend))
14018
14536
  return;
@@ -14254,7 +14772,7 @@ async function spawnCli(cfg, opts = {}) {
14254
14772
  log('Suppressed claude_exit for intentional in-worker restart');
14255
14773
  }
14256
14774
  else {
14257
- send({ type: 'claude_exit', code, signal, logTail, canParkDiagnostic, turnId: exitedTurnId, dispatchAttempt: exitedDispatchAttempt });
14775
+ send({ type: 'claude_exit', code, signal, logTail, canParkDiagnostic, turnId: exitedTurnId, dispatchAttempt: exitedDispatchAttempt, ...(lastInitConfig?.cliId === 'codex-app' && code === CODEX_APP_ACTIVE_WRITER_EXIT_CODE ? { codexAppActiveWriter: true } : {}) });
14258
14776
  }
14259
14777
  });
14260
14778
  const isPersistentBackendReattach = actuallyReattachedPersistent;
@@ -14524,8 +15042,8 @@ function killCli(opts = {}) {
14524
15042
  appRunnerControlDecoder.reset();
14525
15043
  }
14526
15044
  async function restartCliProcess(reason, opts = {}) {
14527
- if (lastInitConfig?.adoptMode) {
14528
- log(`Restart ignored in adopt mode (${reason})`);
15045
+ if (lastInitConfig?.adoptMode || lastInitConfig?.existingAppServerEndpoint) {
15046
+ log(`Restart ignored in shared-adopt mode (${reason})`);
14529
15047
  return;
14530
15048
  }
14531
15049
  // Invalidate queued/deferred callbacks before async teardown begins. Waiting
@@ -14718,8 +15236,29 @@ function startWebServer(host, preferredPort) {
14718
15236
  res.end('Forbidden');
14719
15237
  return;
14720
15238
  }
15239
+ // #933 回归修复:平台注入的 Cookie/Role 会被中央前门剥掉(P1-6 / 内部 grant),
15240
+ // 此时 platformReadonly 判不出来;前门改用这个展示层提示头把「平台认证过的只读
15241
+ // 访客」这一事实带过来。只影响只读页渲染哪条横幅(SSO 登录引导 vs 纯只读提示),
15242
+ // 不参与任何授权判定——hasRead/hasWrite 在它之前就已定死。
15243
+ const platformReadonlyHint = !hasWrite
15244
+ && req.headers[TERMINAL_PLATFORM_READONLY_HINT_HEADER] !== undefined;
14721
15245
  const loginHdr = req.headers['x-botmux-login-url'];
14722
- const loginUrl = typeof loginHdr === 'string' && /^https?:\/\/[^"'<>\s]+$/.test(loginHdr) ? loginHdr : '';
15246
+ // The central front proxy only injects X-Botmux-Login-Url on the SPA 401
15247
+ // path, never on `/s/` terminal requests — so a read-only web terminal
15248
+ // showed an owner-login banner that was a dead <div> (no href). When the
15249
+ // header is absent, self-derive the platform owner-login URL for THIS
15250
+ // session's terminal path: `/open/<machineId>?next=/s/<sessionId>`. The
15251
+ // `/s/` next routes the platform to the terminal subdomain surface and,
15252
+ // after login, lands the browser back on this terminal WITH the host-only
15253
+ // proxy-session cookie the owner check requires (the cross-subdomain SSO
15254
+ // cookie alone does not make the request writable). Returns '' when this
15255
+ // machine is unbound or remote access is off, so the banner falls back to
15256
+ // its non-link text form rather than a broken link.
15257
+ const headerLoginUrl = typeof loginHdr === 'string' && /^https?:\/\/[^"'<>\s]+$/.test(loginHdr)
15258
+ ? loginHdr
15259
+ : '';
15260
+ const loginUrl = headerLoginUrl
15261
+ || (sessionId ? deriveTerminalLoginUrl(sessionId) : '');
14723
15262
  // Herdr snapshots contain screen cells but not terminal mode state. On a
14724
15263
  // refreshed page an alt-screen CLI would otherwise look like an empty
14725
15264
  // normal buffer until resize causes a redraw. Preserve that mode so
@@ -14743,7 +15282,7 @@ function startWebServer(host, preferredPort) {
14743
15282
  || effectiveBackendType === 'tmux'
14744
15283
  || effectiveBackendType === 'zellij';
14745
15284
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
14746
- res.end(getTerminalHtml(hasWrite, platformReadonly, loginUrl, forceRemoteScroll, localTerminalBackend, allowReadOnlyRemoteScroll));
15285
+ res.end(getTerminalHtml(hasWrite, platformReadonly || platformReadonlyHint, loginUrl, forceRemoteScroll, localTerminalBackend, allowReadOnlyRemoteScroll));
14747
15286
  });
14748
15287
  wss = new WebSocketServer({
14749
15288
  server: httpServer,
@@ -14799,6 +15338,26 @@ function startWebServer(host, preferredPort) {
14799
15338
  if (hasWrite)
14800
15339
  authedClients.add(ws);
14801
15340
  log(`WS client connected (total: ${wsClients.size}, write: ${hasWrite})`);
15341
+ // The page only knows the verdict of its own HTTP GET. This upgrade was a
15342
+ // SECOND, independent authorization — and an iOS WebView does not send
15343
+ // cookies on it, so the very same page can be judged writable over HTTP
15344
+ // and read-only here. Tell the page what THIS socket actually got: it
15345
+ // stops forwarding input it knows will be dropped, and reports the truth
15346
+ // up to whatever embeds it (the Workbench terminal pane reads it to label
15347
+ // the channel).
15348
+ //
15349
+ // It used to ride the PTY byte stream as an OSC sequence, which the page
15350
+ // then scanned for on EVERY frame — so any process running inside a
15351
+ // read-only terminal could print the same bytes and flip the page's own
15352
+ // write verdict (or DoS itself into read-only). It is now an out-of-band
15353
+ // control frame that MUST be the first message on this socket, sent in the
15354
+ // same synchronous tick that registered it: the page only ever decodes
15355
+ // frame #1 of a connection as control, so PTY output can never be mistaken
15356
+ // for it. Nothing awaits between `wsClients.add` above and this send.
15357
+ try {
15358
+ ws.send(terminalWriteFrame(hasWrite));
15359
+ }
15360
+ catch { /* already closing */ }
14802
15361
  // A signed Dashboard grant is fixed-expiry — for READ scope as much as
14803
15362
  // for write (P1-5). Even if the central proxy's socket invalidation is
14804
15363
  // delayed or bypassed, the worker independently removes any granted
@@ -15254,6 +15813,18 @@ ${loginUrl ? `<a id="login-banner" href="${loginUrl}" target="_top" rel="noopene
15254
15813
  var isTouch='ontouchstart'in window||navigator.maxTouchPoints>0;
15255
15814
  if(isTouch){document.body.classList.add('touch');}
15256
15815
  var hasToken=${hasWrite};
15816
+ // hasToken 是**这一次 HTTP GET** 的判定,只当初值。真正说了算的是下面这条:worker 在
15817
+ // WebSocket 握手完成时把「这条连接实际拿到什么权限」发下来(OSC 1989 write),落成
15818
+ // wsHasWrite。两者会不一致——iOS WebView 的 WS 升级不带 Cookie,页面 HTTP 判可写、
15819
+ // WS 却只读,照抄 hasToken 就会让人对着一个不收字的终端打字。null = 还没连上。
15820
+ var wsHasWrite=null;
15821
+ // 「这条连接拿到什么权限」的解码器 —— 与 worker 侧同一份实现,原样嵌进来(这段页面
15822
+ // 代码住在模板字符串里,import 不进来,所以只能一份源码两处跑)。
15823
+ // 规则:只在**本连接的第一帧**上尝试,且要求整帧精确匹配。PTY 里跑的进程打出一模
15824
+ // 一样的字节也翻不动权限——它永远不可能是第一帧。
15825
+ var _wbDecodeWriteFrame=${decodeTerminalWriteFrameSource};
15826
+ // 本连接还没消费过首帧。每次 connect() 重置:上一条连接的结论套不到新连接上。
15827
+ var _wbFirstFrame=true;
15257
15828
  var platformReadonly=${platformReadonly};
15258
15829
  var remoteScroll=${forceRemoteScroll};
15259
15830
  var localTerminalBackend=${localTerminalBackend};
@@ -15331,6 +15902,39 @@ function _wbApplyAutoFontSize(){
15331
15902
  return true;
15332
15903
  }
15333
15904
  fit.fit();
15905
+ // 这条通道到底能不能写,以**已经建立的 WS** 为准(见页面顶部 wsHasWrite 那段注释)。
15906
+ // 结论一到手就做两件事:① 页面自己收起输入,别把注定被丢掉的字发出去;② 上抛给嵌入
15907
+ // 方(工作台终端面板据此显示「可输入 / 只读」,而不是照抄 HTTP 的 hasToken)。
15908
+ // 上抛的目标 origin 绝不用星号:优先取 location.ancestorOrigins(WebKit / Blink 都有,
15909
+ // iOS WebView 正是 WebKit),取不到就退回「父页给我们发外观消息时带的那个 origin」;
15910
+ // 两者都没有就不发——宁可不上抛,也不广播出去。
15911
+ // 注意:这一整段在 worker.ts 的模板字符串里,注释和代码都不能出现反引号,
15912
+ // 也不能出现「美元号 + 左花括号」的插值起始序列,否则会把模板字符串截断。
15913
+ var _wbParentOrigin=null;
15914
+ try{
15915
+ var _wbAo=window.location.ancestorOrigins;
15916
+ if(_wbAo&&_wbAo.length&&_wbAo[0]&&_wbAo[0]!=='null')_wbParentOrigin=_wbAo[0];
15917
+ }catch(_e){}
15918
+ function _wbPostWrite(){
15919
+ if(window.parent===window||!_wbParentOrigin)return;
15920
+ // wsHasWrite===null 也要上抛:那是「这条连接退回未知」的一次明确通知(重连开始时
15921
+ // 发),嵌入方据此把上一条连接的结论清掉,而不是继续显示一个过期的判定。
15922
+ try{window.parent.postMessage({type:'botmux:wb-terminal-write',write:wsHasWrite},_wbParentOrigin)}catch(_e){}
15923
+ }
15924
+ function _wbSetWsWrite(v){
15925
+ var changed=wsHasWrite!==v;
15926
+ wsHasWrite=v;
15927
+ if(changed){
15928
+ var _wb=document.getElementById('readonly-banner');
15929
+ // 页面以为自己可写、这条连接其实只读:把只读横幅亮出来。反过来(连上后确认可写)
15930
+ // 就把它收掉,别留一条与事实相反的提示。null(未知)两边都不动:还没有依据。
15931
+ if(_wb&&!platformReadonly){
15932
+ if(v===false&&hasToken)_wb.classList.add('show');
15933
+ else if(v===true)_wb.classList.remove('show');
15934
+ }
15935
+ }
15936
+ _wbPostWrite();
15937
+ }
15334
15938
  // 工作台下发的终端外观(消息类型 botmux:wb-appearance)。终端画布住在这个跨文档
15335
15939
  // iframe 里,父页换 class / 换 CSS 变量都传不进来,配色只能靠 postMessage 递过来。
15336
15940
  // 注意:这一整段在 worker.ts 的模板字符串里,注释和代码都不能出现反引号,
@@ -15354,17 +15958,22 @@ window.addEventListener('message',function(_ev){
15354
15958
  if(_ev.source===window||_ev.source!==window.parent)return;
15355
15959
  var _d=_ev.data;
15356
15960
  if(!_d||_d.type!=='botmux:wb-appearance')return;
15961
+ // 这一条消息本身就证明了父页的 origin(来源已经校验过是 window.parent)。
15962
+ // ancestorOrigins 拿不到时(Firefox)靠它补上,并把攒着的写权限结论补发一次。
15963
+ if(!_wbParentOrigin&&_ev.origin&&_ev.origin!=='null'){_wbParentOrigin=_ev.origin;_wbPostWrite();}
15357
15964
  var _theme=_wbSanitizeTheme(_d.theme);
15358
15965
  if(!_theme)return;
15359
15966
  try{
15360
15967
  term.options.theme=_theme;
15361
- // 阅读风是大行距 1.3;经典保持 xterm 默认的 1,「经典 = 原样」的一部分。
15968
+ // 阅读风走 1.15 的行距;经典保持 xterm 默认的 1,「经典 = 原样」的一部分。
15362
15969
  // 经典那套色值与上面写死的字面量逐色相同,所以没开阅读风的用户零变化。
15363
15970
  // 这两个数字的唯一出处是 WORKBENCH_TERM_LINE_HEIGHTS(agent-workbench-appearance.ts),
15364
15971
  // 接缝测试按那张表比对这一行的字面量:改一处必须两处一起改。
15365
15972
  // 曾经是 1.55,单元格高约 24.5px,CLI 底部那块固定 8 行的 chrome(提示 + 输入框 +
15366
- // 状态条)就要占掉约 196px —— 一屏四分之一全是 chrome,正文被挤走。
15367
- term.options.lineHeight=_d.termStyle==='reader'?1.3:1;
15973
+ // 状态条)就要占掉约 196px —— 一屏四分之一全是 chrome,正文被挤走,所以先降到 1.3。
15974
+ // 1.3 对中文密集的 TUI 内容还是偏松:汉字撑满 em 框,行间空隙观感接近隔行,于是
15975
+ // 再收一档到 1.15(单元格高约 18.2px),比经典松一成半,读起来才连成一段。
15976
+ term.options.lineHeight=_d.termStyle==='reader'?1.15:1;
15368
15977
  // 行距变了可视行数就变,必须复算一次。几何契约不动:只有画布内重绘。
15369
15978
  fit.fit();
15370
15979
  }catch(_e){}
@@ -15550,10 +16159,17 @@ function _sendInput(d){if(ws_&&ws_.readyState===1)ws_.send(JSON.stringify({type:
15550
16159
  var _MOTION_RE=/^(?:\\x1b\\[<(?:35|39|43|47|51|55|59|63);\\d+;\\d+[Mm])+$/;
15551
16160
  var _motionPend=null,_motionT=0;
15552
16161
  term.onData(function(d){
15553
- if(!hasToken){
16162
+ // 只有已建立的 WS 明确回报「可写」(wsHasWrite===true) 才放行输入。null(还没确认,
16163
+ // 含重连中)和 false(这条连接判成只读)一律不发——桌面也要等首帧。照抄 hasToken
16164
+ // (这一次 HTTP GET 的判定)会在 WS 不带 Cookie 的 iOS WebView 上让人对着一个不收字
16165
+ // 的终端打字,按键石沉大海最难自查(第 17 点)。
16166
+ if(wsHasWrite!==true){
15554
16167
  // Mouse escape sequences are input too: a TUI can bind clicks or wheel
15555
16168
  // events to actions. View links never forward terminal bytes.
15556
- _showReadonlyToast();return;
16169
+ // 已经确定只读(这条连接判 false,或 HTTP GET 本来就没给写)才弹只读提示;null 是
16170
+ // 还没连上的短暂过渡,此时 ws 多半也没 OPEN,静默丢弃即可,别闪一条与事实相反的只读。
16171
+ if(!hasToken||wsHasWrite===false)_showReadonlyToast();
16172
+ return;
15557
16173
  }
15558
16174
  if(_MOTION_RE.test(d)){
15559
16175
  // Trailing throttle: keep only the LATEST motion, flush every 90ms. Hover
@@ -15614,6 +16230,10 @@ if(typeof ResizeObserver!=='undefined'){
15614
16230
  var proto=location.protocol==='https:'?'wss':'ws';
15615
16231
  var ws=new WebSocket(proto+'://'+location.host+base+'/'+location.search);
15616
16232
  ws_=ws;ws.binaryType='arraybuffer';
16233
+ // 新连接 = 新的一次鉴权。上一条连接拿到的权限说明不了这一条,先退回未知(并把这
16234
+ // 个「未知」上抛给嵌入方),等这条连接自己的首帧说话。
16235
+ _wbFirstFrame=true;
16236
+ _wbSetWsWrite(null);
15617
16237
  // Force a resize on every (re)connect: clear the dedup memory first. On
15618
16238
  // reconnect the browser grid is usually unchanged, so without this the
15619
16239
  // dedup in sendResize() would suppress the resize — but a reconnect often
@@ -15623,6 +16243,14 @@ if(typeof ResizeObserver!=='undefined'){
15623
16243
  // (status-line update bleeds into the line below). Always re-assert size.
15624
16244
  ws.onopen=function(){el.textContent='connected';el.className='ok';_lastC=_lastR=0;sendResize()};
15625
16245
  ws.onmessage=function(e){
16246
+ // 带外控制帧只可能是**本连接的第一帧**,而且必须整帧精确匹配(见页面顶部
16247
+ // _wbDecodeWriteFrame 那段注释)。首帧一旦消费掉,这条连接上之后的每一个字节
16248
+ // 就都只是 PTY 输出 —— 里面的进程再怎么打印控制帧的字面量也翻不动权限。
16249
+ if(_wbFirstFrame){
16250
+ _wbFirstFrame=false;
16251
+ var _ctl=_wbDecodeWriteFrame(e.data,true);
16252
+ if(_ctl!==null){_wbSetWsWrite(_ctl);return;}
16253
+ }
15626
16254
  var data=typeof e.data==='string'?e.data:new TextDecoder().decode(e.data);
15627
16255
  // Snapshot-aware Herdr history replaces the buffer instead of appending a
15628
16256
  // mostly-overlapping full screen. Preserve the reader's anchor when older
@@ -15663,7 +16291,15 @@ if(typeof ResizeObserver!=='undefined'){
15663
16291
  if(m){try{_clipBuf=new TextDecoder().decode(Uint8Array.from(atob(m[1]),function(c){return c.charCodeAt(0)}));_doCopy(_clipBuf);_showCopied()}catch(ex){}}
15664
16292
  term.write(data,_settleInitialBottom);
15665
16293
  };
15666
- ws.onclose=function(){ws_=null;el.textContent='disconnected';el.className='err';setTimeout(connect,2000)};
16294
+ ws.onclose=function(){
16295
+ ws_=null;el.textContent='disconnected';el.className='err';
16296
+ // 关闭当下就把这条连接的写权限退回未知:先复位首帧标志(重连后要等新首帧才恢复
16297
+ // 结论),再 _wbSetWsWrite(null) —— 它同时收起输入(term.onData/toolbar/_fwdScroll
16298
+ // 的门禁都以 wsHasWrite===true 为准)并向嵌入方上抛 write:null。不这样做的话,断线
16299
+ // 到 2 秒后重连的空窗里,页面仍以上一条连接的旧判定放行输入、父页也还显示旧的可写。
16300
+ _wbFirstFrame=true;_wbSetWsWrite(null);
16301
+ setTimeout(connect,2000);
16302
+ };
15667
16303
  ws.onerror=function(){ws.close()};
15668
16304
  })();
15669
16305
 
@@ -15734,6 +16370,12 @@ function _cellAt(clientX,clientY){
15734
16370
  }
15735
16371
  function _fwdScroll(px,coord){
15736
16372
  if((!hasToken&&!readOnlyRemoteScroll)||!ws_||ws_.readyState!==1||!px)return;
16373
+ // 写链路(hasToken)转发的是 type:'input',与 term.onData / toolbar 同一条判据:只有已
16374
+ // 建立的 WS 明确回报可写(wsHasWrite===true)才放行;null(未确认,含重连中)与 false
16375
+ // (这条连接只读)一律不发——照抄 hasToken 会在 WS 不带 Cookie 时把输入打进一个原地
16376
+ // 丢字的终端(第 17 点)。只读远程滚动(hasToken=false)发的是 type:'scroll',不是输入,
16377
+ // 不受这道门约束(服务端另有校验)。
16378
+ if(hasToken&&wsHasWrite!==true)return;
15737
16379
  coord=coord||(((term.cols>>1)+1)+';'+((term.rows>>1)+1)); // never (1,1)
15738
16380
  var dir=px<0?-1:1;
15739
16381
  if(_scrollBurstDir&&dir!==_scrollBurstDir){_scrollAccum=0;_scrollBurstTicks=0;}
@@ -15978,6 +16620,9 @@ if(isTouch&&hasToken){
15978
16620
  var press=null;
15979
16621
  function fire(){
15980
16622
  if(!ws_||ws_.readyState!==1)return;
16623
+ // 与 term.onData 同一条判据:只有 WS 明确回报可写(wsHasWrite===true)才发;null
16624
+ // (还没确认)与 false(只读)都不发,桌面也等首帧(第 17 点)。
16625
+ if(wsHasWrite!==true){if(wsHasWrite===false)_showReadonlyToast();return;}
15981
16626
  var k=km[btn.getAttribute('data-k')];
15982
16627
  if(k)ws_.send(JSON.stringify({type:'input',data:k}));
15983
16628
  }
@@ -16350,6 +16995,10 @@ process.on('message', async (raw) => {
16350
16995
  if (msg.locale === 'zh' || msg.locale === 'en') {
16351
16996
  setDefaultLocale(msg.locale);
16352
16997
  }
16998
+ // Wire user prompt-key overrides into t() so worker-side prompt building
16999
+ // (claude-family `--append-system-prompt` via buildBotmuxSystemPromptText,
17000
+ // mojo catalog, etc.) honours customizations. Idempotent per worker.
17001
+ registerPromptOverrideResolver();
16353
17002
  // Scope session store to this bot's per-bot file.
16354
17003
  // Slice C0: workflow-spawned workers (BOTMUX_WORKFLOW=1) skip this —
16355
17004
  // their `sessionId` is synthetic (`wf-<runId>-<activityId>-...`) and
@@ -16800,12 +17449,23 @@ process.on('message', async (raw) => {
16800
17449
  // shell。bareShellCheckInProgress 覆盖“检查进行中”、bareShellLaunchBlocked
16801
17450
  // 覆盖“仍停在裸 shell 的安全 hold”两种状态,一并入队;若该进程随后出现
16802
17451
  // 真实 PTY prompt 且 leaf 已变为非 shell,markPromptReady 会恢复排空。
17452
+ // Hook-review menus are another direct-write hazard: raw_input normally
17453
+ // preserves busy delivery, but its Enter/arrow sequence must never drive
17454
+ // the menu's current selection.
17455
+ refreshHookReviewInputHold(lastAnalyzerSnapshot || renderer?.rawSnapshot() || '');
16803
17456
  if (cliRestartInProgress || rawInputRestartGate || sessionRenameInFlight()
16804
17457
  || shouldHoldCodexRunnerInput(codexRunnerFreshness)
16805
17458
  || injectionFlushing || shouldDeferUserFlush(pendingInjections)
16806
- || bareShellCheckInProgress || bareShellLaunchBlocked) {
17459
+ || bareShellCheckInProgress || bareShellLaunchBlocked
17460
+ || hookReviewInputHold) {
16807
17461
  freshnessInputQueue.enqueueRaw(msg);
16808
- log(`Deferred passthrough slash command until CLI input gate settles: ${msg.content}`);
17462
+ if (hookReviewInputHold) {
17463
+ notifyHookReviewInputHold();
17464
+ log(`Deferred passthrough slash command while Hook review is visible: ${msg.content}`);
17465
+ }
17466
+ else {
17467
+ log(`Deferred passthrough slash command until CLI input gate settles: ${msg.content}`);
17468
+ }
16809
17469
  }
16810
17470
  else {
16811
17471
  await deliverRawInput(msg);
@@ -17198,6 +17858,15 @@ process.on('message', async (raw) => {
17198
17858
  log('Late ready signal after timeout fallback — marking prompt ready now');
17199
17859
  markPromptReady();
17200
17860
  }
17861
+ // CLI 启动确证(SessionStart hook 已跑):复位连续重启计数——resume 目标
17862
+ // 显然存在且加载成功。这和 prompt 侧复位是同一语义,但更早:2026-08-20
17863
+ // 事故里 resume 后的 CLI 已 ready 二十多秒、还没等到 prompt 复位就被共享
17864
+ // server 抖动误杀,第二次重启因此被 tier-2 强制 FRESH 白丢上下文。到过
17865
+ // ready 的 spawn 崩溃不是「resume 目标不存在」问题,下一轮应有全新预算。
17866
+ if (consecutiveInWorkerRestarts > 0) {
17867
+ log(`SessionStart ready — resetting consecutive restart count (was ${consecutiveInWorkerRestarts})`);
17868
+ consecutiveInWorkerRestarts = 0;
17869
+ }
17201
17870
  if (msg.requestId) {
17202
17871
  send({ type: 'session_ready_ack', requestId: msg.requestId });
17203
17872
  }
@@ -17359,11 +18028,13 @@ process.on('message', async (raw) => {
17359
18028
  catch { /* logged by backend */ }
17360
18029
  }
17361
18030
  killCli();
17362
- // Bridge marker file outlives a single CLI process (we keep it across
17363
- // restarts so a mid-flight send is still credited), but a real close
17364
- // tears down the session purge the file so a future re-use of the
17365
- // same sessionId starts clean.
18031
+ // Bridge marker + turn-journal files outlive a single CLI process (kept
18032
+ // across restarts so a mid-flight send is still credited and an
18033
+ // interrupted turn can be restored), but a real close tears down the
18034
+ // session — purge both so a future re-use of the same sessionId starts
18035
+ // clean.
17366
18036
  clearSendMarkers();
18037
+ clearBridgeTurnJournalFile();
17367
18038
  cleanup();
17368
18039
  process.exit(0);
17369
18040
  }
@@ -17536,6 +18207,7 @@ process.on('message', async (raw) => {
17536
18207
  stopCodexBridge();
17537
18208
  killCli();
17538
18209
  clearSendMarkers();
18210
+ clearBridgeTurnJournalFile();
17539
18211
  cleanup();
17540
18212
  process.exit(0);
17541
18213
  }
@@ -17695,10 +18367,29 @@ function cleanup() {
17695
18367
  // cleanup retires the lease. SIGKILL residue is reclaimed by the next worker.
17696
18368
  releaseCodexAppPosixOwnerLease();
17697
18369
  }
17698
- process.on('SIGTERM', () => { stopScreenshotLoop(); killCli(); cleanup(); process.exit(0); });
17699
- process.on('SIGINT', () => { stopScreenshotLoop(); killCli(); cleanup(); process.exit(0); });
18370
+ /**
18371
+ * A shared existing-App-Server session owns this worker and its bmx-* remote
18372
+ * TUI, but never owns the App Server/thread behind that TUI. On daemon
18373
+ * restart, preserve the remote TUI so the replacement daemon can reattach it;
18374
+ * only retire this worker's HTTP/WebSocket observers. Ordinary managed sessions
18375
+ * retain the historical killCli shutdown path.
18376
+ */
18377
+ function shutdownWorkerForParentExit(reason) {
18378
+ stopScreenshotLoop();
18379
+ if (lastInitConfig?.existingAppServerEndpoint) {
18380
+ log(`Preserving existing-App-Server remote TUI during ${reason}`);
18381
+ cleanup();
18382
+ process.exit(0);
18383
+ return;
18384
+ }
18385
+ killCli();
18386
+ cleanup();
18387
+ process.exit(0);
18388
+ }
18389
+ process.on('SIGTERM', () => shutdownWorkerForParentExit('SIGTERM'));
18390
+ process.on('SIGINT', () => shutdownWorkerForParentExit('SIGINT'));
17700
18391
  // If parent daemon dies, IPC channel closes — clean up
17701
- process.on('disconnect', () => { log('Daemon disconnected'); stopScreenshotLoop(); killCli(); cleanup(); process.exit(0); });
18392
+ process.on('disconnect', () => { log('Daemon disconnected'); shutdownWorkerForParentExit('IPC disconnect'); });
17702
18393
  // Watchdog: belt-and-braces parent-death detection. SIGTERM and 'disconnect'
17703
18394
  // should both reach us when the daemon dies, but if main thread is stuck in
17704
18395
  // a sync path V8 silently buffers the signal and we end up as a ppid=1
@@ -17715,16 +18406,12 @@ setInterval(() => {
17715
18406
  const currentPpid = process.ppid;
17716
18407
  if (currentPpid !== ORIGINAL_PARENT_PID || currentPpid === 1) {
17717
18408
  log(`Watchdog: parent pid changed (${ORIGINAL_PARENT_PID} → ${currentPpid}) — daemon died, exiting`);
17718
- stopScreenshotLoop();
17719
18409
  try {
17720
- killCli();
18410
+ shutdownWorkerForParentExit('parent watchdog');
17721
18411
  }
17722
- catch { /* best-effort */ }
17723
- try {
17724
- cleanup();
18412
+ catch {
18413
+ process.exit(0); /* best-effort exit if teardown itself faults */
17725
18414
  }
17726
- catch { /* best-effort */ }
17727
- process.exit(0);
17728
18415
  }
17729
18416
  }, 30_000).unref();
17730
18417
  // ─── Sandbox crash-time teardown ─────────────────────────────────────────────