@opengeni/react 0.57.0 → 1.0.2-canary.37191796234001

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 (629) hide show
  1. package/README.md +654 -22
  2. package/dist/accounts.d.ts +64 -0
  3. package/dist/accounts.js +822 -0
  4. package/dist/accounts.js.map +1 -0
  5. package/dist/approvals.d.ts +2 -1
  6. package/dist/artifacts-document.js +3 -2
  7. package/dist/artifacts-presentation.js +3 -2
  8. package/dist/artifacts-spreadsheet.js +2 -2
  9. package/dist/artifacts.d.ts +4 -1
  10. package/dist/artifacts.js +518 -42
  11. package/dist/artifacts.js.map +1 -1
  12. package/dist/browser-viewer-GTR5XQ7X.js +16 -0
  13. package/dist/capability-catalog-row.d.ts +20 -0
  14. package/dist/capability-logo-fallback.d.ts +3 -0
  15. package/dist/chat-interactive-block-UGBTNB3L.js +11 -0
  16. package/dist/chat-interactive-block-UGBTNB3L.js.map +1 -0
  17. package/dist/{chunk-5UQPNLIO.js → chunk-23J2ZNXT.js} +4117 -3896
  18. package/dist/chunk-23J2ZNXT.js.map +1 -0
  19. package/dist/chunk-447JLCBU.js +209 -0
  20. package/dist/chunk-447JLCBU.js.map +1 -0
  21. package/dist/chunk-4RVYOY4O.js +379 -0
  22. package/dist/chunk-4RVYOY4O.js.map +1 -0
  23. package/dist/chunk-5CVXZA3Y.js +292 -0
  24. package/dist/chunk-5CVXZA3Y.js.map +1 -0
  25. package/dist/chunk-5J4FOW6Q.js +108 -0
  26. package/dist/chunk-5J4FOW6Q.js.map +1 -0
  27. package/dist/chunk-A2PI6UMY.js +41 -0
  28. package/dist/chunk-A2PI6UMY.js.map +1 -0
  29. package/dist/{chunk-7WCLAEFV.js → chunk-A4UJB2MM.js} +37 -27
  30. package/dist/chunk-A4UJB2MM.js.map +1 -0
  31. package/dist/chunk-B3V6TG77.js +41 -0
  32. package/dist/chunk-B3V6TG77.js.map +1 -0
  33. package/dist/chunk-BRM6FVZA.js +302 -0
  34. package/dist/chunk-BRM6FVZA.js.map +1 -0
  35. package/dist/chunk-C5X5AJXK.js +182 -0
  36. package/dist/chunk-C5X5AJXK.js.map +1 -0
  37. package/dist/chunk-CR7U7HAR.js +453 -0
  38. package/dist/chunk-CR7U7HAR.js.map +1 -0
  39. package/dist/{chunk-R3AZOAB5.js → chunk-DYWBUX5Q.js} +29 -123
  40. package/dist/chunk-DYWBUX5Q.js.map +1 -0
  41. package/dist/chunk-E3G7SAYZ.js +3124 -0
  42. package/dist/chunk-E3G7SAYZ.js.map +1 -0
  43. package/dist/chunk-ELCE5SXV.js +359 -0
  44. package/dist/chunk-ELCE5SXV.js.map +1 -0
  45. package/dist/chunk-ERGYJIBB.js +21 -0
  46. package/dist/chunk-ERGYJIBB.js.map +1 -0
  47. package/dist/chunk-FFA2OVOV.js +319 -0
  48. package/dist/chunk-FFA2OVOV.js.map +1 -0
  49. package/dist/chunk-FZCP527Y.js +116 -0
  50. package/dist/chunk-FZCP527Y.js.map +1 -0
  51. package/dist/chunk-GIMRELBW.js +14 -0
  52. package/dist/chunk-GIMRELBW.js.map +1 -0
  53. package/dist/{chunk-6KLX2RH5.js → chunk-H2NJQORO.js} +1069 -79
  54. package/dist/chunk-H2NJQORO.js.map +1 -0
  55. package/dist/chunk-I7IWRQLN.js +1608 -0
  56. package/dist/chunk-I7IWRQLN.js.map +1 -0
  57. package/dist/{chunk-HA46CYQK.js → chunk-IQUCRAT7.js} +792 -537
  58. package/dist/chunk-IQUCRAT7.js.map +1 -0
  59. package/dist/chunk-IWIDV7SB.js +1 -0
  60. package/dist/chunk-IWIDV7SB.js.map +1 -0
  61. package/dist/chunk-J2QWVSQY.js +312 -0
  62. package/dist/chunk-J2QWVSQY.js.map +1 -0
  63. package/dist/chunk-JFNEV4LU.js +2733 -0
  64. package/dist/chunk-JFNEV4LU.js.map +1 -0
  65. package/dist/chunk-JT4JW7BC.js +54 -0
  66. package/dist/chunk-JT4JW7BC.js.map +1 -0
  67. package/dist/chunk-K3HQAKQX.js +24 -0
  68. package/dist/chunk-K3HQAKQX.js.map +1 -0
  69. package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
  70. package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
  71. package/dist/chunk-NZUI76EM.js +371 -0
  72. package/dist/chunk-NZUI76EM.js.map +1 -0
  73. package/dist/{chunk-PEI6RANI.js → chunk-PNY3JKBJ.js} +309 -74
  74. package/dist/chunk-PNY3JKBJ.js.map +1 -0
  75. package/dist/{chunk-IGC6A2ZI.js → chunk-Q5QBEZIQ.js} +777 -246
  76. package/dist/chunk-Q5QBEZIQ.js.map +1 -0
  77. package/dist/chunk-RHREZ7BO.js +11 -0
  78. package/dist/chunk-RHREZ7BO.js.map +1 -0
  79. package/dist/chunk-SCOTPE2I.js +589 -0
  80. package/dist/chunk-SCOTPE2I.js.map +1 -0
  81. package/dist/{chunk-B6WPZSTT.js → chunk-SJZDKVW2.js} +1331 -1163
  82. package/dist/chunk-SJZDKVW2.js.map +1 -0
  83. package/dist/chunk-TMJRG4YC.js +12803 -0
  84. package/dist/chunk-TMJRG4YC.js.map +1 -0
  85. package/dist/chunk-TMYGL6X5.js +742 -0
  86. package/dist/chunk-TMYGL6X5.js.map +1 -0
  87. package/dist/chunk-UGLEYEMP.js +33 -0
  88. package/dist/chunk-UGLEYEMP.js.map +1 -0
  89. package/dist/chunk-UK4TINKT.js +27 -0
  90. package/dist/chunk-UK4TINKT.js.map +1 -0
  91. package/dist/chunk-UTRTG6LH.js +220 -0
  92. package/dist/chunk-UTRTG6LH.js.map +1 -0
  93. package/dist/chunk-WEHMPOXK.js +39 -0
  94. package/dist/chunk-WEHMPOXK.js.map +1 -0
  95. package/dist/chunk-WRHIZYZF.js +988 -0
  96. package/dist/chunk-WRHIZYZF.js.map +1 -0
  97. package/dist/{chunk-HUIKIDM5.js → chunk-WUISZ3WU.js} +223 -115
  98. package/dist/chunk-WUISZ3WU.js.map +1 -0
  99. package/dist/chunk-XCHWDJ22.js +107 -0
  100. package/dist/chunk-XCHWDJ22.js.map +1 -0
  101. package/dist/chunk-XOAMRDDK.js +481 -0
  102. package/dist/chunk-XOAMRDDK.js.map +1 -0
  103. package/dist/chunk-XUW2UWQM.js +299 -0
  104. package/dist/chunk-XUW2UWQM.js.map +1 -0
  105. package/dist/{chunk-KCGXPP5V.js → chunk-Y2PRMYLC.js} +11 -6
  106. package/dist/chunk-Y2PRMYLC.js.map +1 -0
  107. package/dist/chunk-YBXQDQFX.js +1772 -0
  108. package/dist/chunk-YBXQDQFX.js.map +1 -0
  109. package/dist/{chunk-KZ73RL76.js → chunk-YQEAA6LT.js} +7 -3
  110. package/dist/chunk-YQEAA6LT.js.map +1 -0
  111. package/dist/chunk-YV4L3ILF.js +9 -0
  112. package/dist/chunk-YV4L3ILF.js.map +1 -0
  113. package/dist/{chunk-LLOLUHVG.js → chunk-YWFG7ZX7.js} +1358 -2746
  114. package/dist/chunk-YWFG7ZX7.js.map +1 -0
  115. package/dist/chunk-ZYRGEKWS.js +216 -0
  116. package/dist/chunk-ZYRGEKWS.js.map +1 -0
  117. package/dist/client.d.ts +7 -4
  118. package/dist/clipboard.js +1 -0
  119. package/dist/commands/types.d.ts +2 -0
  120. package/dist/components/anthropic-mark.d.ts +3 -0
  121. package/dist/components/approval-surface.d.ts +15 -0
  122. package/dist/components/artifacts/artifact-chrome.d.ts +120 -0
  123. package/dist/components/artifacts/artifact-sandbox.d.ts +27 -0
  124. package/dist/components/artifacts/artifact-surface.d.ts +3 -1
  125. package/dist/components/artifacts/chat-interactive-block.d.ts +29 -0
  126. package/dist/components/artifacts/deferred-chat-media.d.ts +15 -0
  127. package/dist/components/artifacts/document-editor.d.ts +4 -2
  128. package/dist/components/artifacts/editable-artifact-ui.d.ts +2 -1
  129. package/dist/components/artifacts/editable-artifact-view.d.ts +46 -0
  130. package/dist/components/artifacts/editable-document.d.ts +1 -1
  131. package/dist/components/artifacts/editable-presentation.d.ts +2 -2
  132. package/dist/components/artifacts/editable-spreadsheet.d.ts +2 -1
  133. package/dist/components/artifacts/index.d.ts +8 -1
  134. package/dist/components/artifacts/inline-html-document.d.ts +2 -0
  135. package/dist/components/artifacts/latest-task-runner.d.ts +15 -0
  136. package/dist/components/artifacts/presentation-editor.d.ts +6 -2
  137. package/dist/components/artifacts/published-html-artifact-frame.d.ts +48 -3
  138. package/dist/components/artifacts/session-artifact-viewer.d.ts +39 -0
  139. package/dist/components/artifacts/site-view.d.ts +23 -0
  140. package/dist/components/artifacts/spreadsheet-general-display.d.ts +24 -0
  141. package/dist/components/artifacts/visualization-assets.gen.d.ts +2 -0
  142. package/dist/components/browser-select-control.d.ts +16 -0
  143. package/dist/components/browser-viewer.d.ts +10 -1
  144. package/dist/components/chat-composer.d.ts +21 -3
  145. package/dist/components/child-session-link.d.ts +5 -0
  146. package/dist/components/claude-mark.d.ts +3 -0
  147. package/dist/components/code-editor.d.ts +1 -4
  148. package/dist/components/composer.d.ts +45 -4
  149. package/dist/components/computer-viewer.d.ts +4 -1
  150. package/dist/components/copy-button.d.ts +3 -1
  151. package/dist/components/desktop-viewer.d.ts +1 -1
  152. package/dist/components/enrollment-consent.d.ts +6 -3
  153. package/dist/components/file-browser.d.ts +10 -1
  154. package/dist/components/fleet-tile.d.ts +2 -2
  155. package/dist/components/grok-mark.d.ts +26 -0
  156. package/dist/components/human-input-form.d.ts +21 -2
  157. package/dist/components/human-input-surface.d.ts +6 -3
  158. package/dist/components/machine-card.d.ts +4 -1
  159. package/dist/components/machines/machine-detail.d.ts +4 -1
  160. package/dist/components/machines/machine-operation-policy.d.ts +11 -0
  161. package/dist/components/machines-dashboard.d.ts +3 -1
  162. package/dist/components/markdown-table-layout.d.ts +12 -0
  163. package/dist/components/markdown.d.ts +47 -3
  164. package/dist/components/message-timeline.d.ts +72 -9
  165. package/dist/components/model-mark.d.ts +29 -0
  166. package/dist/components/model-policy-picker-menu.d.ts +9 -0
  167. package/dist/components/model-policy-picker.d.ts +62 -13
  168. package/dist/components/open-geni-chat.d.ts +55 -0
  169. package/dist/components/open-geni-links.d.ts +68 -0
  170. package/dist/components/openrouter-mark.d.ts +6 -0
  171. package/dist/components/pierre-file.d.ts +1 -1
  172. package/dist/components/preview-loading.d.ts +2 -0
  173. package/dist/components/queue-item-content.d.ts +9 -0
  174. package/dist/components/sandbox-files.d.ts +13 -2
  175. package/dist/components/sandbox-workspace.d.ts +43 -3
  176. package/dist/components/session-capability-frame.d.ts +35 -0
  177. package/dist/components/session-chrome.d.ts +66 -8
  178. package/dist/components/session-commands-panel.d.ts +6 -0
  179. package/dist/components/session-commands.d.ts +6 -0
  180. package/dist/components/session-connection-request.d.ts +8 -0
  181. package/dist/components/session-conversation.d.ts +86 -0
  182. package/dist/components/session-list.d.ts +36 -0
  183. package/dist/components/session-mcp-capability-card.d.ts +41 -0
  184. package/dist/components/timeline-anchor.d.ts +24 -0
  185. package/dist/components/timeline-annotation-chrome.d.ts +25 -0
  186. package/dist/components/timeline-annotation-layout.d.ts +42 -0
  187. package/dist/components/timeline-annotation-markers.d.ts +8 -0
  188. package/dist/components/timeline-annotation-reveal-context.d.ts +4 -0
  189. package/dist/components/timeline-annotation-shared.d.ts +31 -0
  190. package/dist/components/timeline-annotations-dialog.d.ts +3 -2
  191. package/dist/components/timeline-annotations.d.ts +13 -3
  192. package/dist/components/timeline-reading-progress.d.ts +11 -0
  193. package/dist/components/timeline-search.d.ts +34 -0
  194. package/dist/components/timeline-settlement.d.ts +16 -0
  195. package/dist/components/tip-follow.d.ts +16 -0
  196. package/dist/components/usage-limit-notice.d.ts +49 -0
  197. package/dist/components/usage-member-list.d.ts +91 -0
  198. package/dist/components/usage-meter.d.ts +73 -0
  199. package/dist/components/use-viewer-menu-dismiss.d.ts +2 -0
  200. package/dist/components/user-message-body.d.ts +10 -1
  201. package/dist/components/workspace-dock.d.ts +6 -2
  202. package/dist/composer.d.ts +5 -4
  203. package/dist/composer.js +15 -9
  204. package/dist/computer-viewer-ENHVAZ2Q.js +19 -0
  205. package/dist/computer-viewer-ENHVAZ2Q.js.map +1 -0
  206. package/dist/connect-account-identity.d.ts +8 -0
  207. package/dist/connect-accounts.d.ts +15 -0
  208. package/dist/connect-chooser.d.ts +18 -0
  209. package/dist/connect-panel.d.ts +18 -0
  210. package/dist/connect-setup.d.ts +16 -0
  211. package/dist/connect.d.ts +21 -0
  212. package/dist/connect.js +2129 -0
  213. package/dist/connect.js.map +1 -0
  214. package/dist/connection-catalog.d.ts +38 -0
  215. package/dist/connection-discovery.d.ts +12 -0
  216. package/dist/connection-installed.d.ts +17 -0
  217. package/dist/connection-logo.d.ts +9 -0
  218. package/dist/connection-service-logo.d.ts +8 -0
  219. package/dist/connection-service-presentation.d.ts +13 -0
  220. package/dist/connection-skeleton.d.ts +5 -0
  221. package/dist/connection-type-picker.d.ts +6 -0
  222. package/dist/conversation-timeline.d.ts +5 -0
  223. package/dist/desktop.d.ts +7 -0
  224. package/dist/desktop.js +26 -0
  225. package/dist/desktop.js.map +1 -0
  226. package/dist/device-authorization.d.ts +23 -0
  227. package/dist/diffs.d.ts +8 -0
  228. package/dist/diffs.js +14 -0
  229. package/dist/diffs.js.map +1 -0
  230. package/dist/discovery-cache.d.ts +5 -0
  231. package/dist/editor.d.ts +7 -0
  232. package/dist/editor.js +21 -0
  233. package/dist/editor.js.map +1 -0
  234. package/dist/embedded-session-client.d.ts +23 -0
  235. package/dist/file-node-visibility.d.ts +15 -0
  236. package/dist/{fleet-decision-row-GGCAT4E4.js → fleet-decision-row-YZ3ZKWMM.js} +2 -1
  237. package/dist/fleet-decision-row-YZ3ZKWMM.js.map +1 -0
  238. package/dist/hooks/channel-cache.d.ts +23 -0
  239. package/dist/hooks/latest-question.d.ts +25 -0
  240. package/dist/hooks/shared-machines-list.d.ts +14 -0
  241. package/dist/hooks/use-attached-browsers.d.ts +2 -1
  242. package/dist/hooks/use-browser-frame-stream.d.ts +1 -1
  243. package/dist/hooks/use-browser-session.d.ts +12 -1
  244. package/dist/hooks/use-channels.d.ts +1 -0
  245. package/dist/hooks/use-codex-accounts.d.ts +25 -7
  246. package/dist/hooks/use-composer.d.ts +45 -9
  247. package/dist/hooks/use-computer-frame-stream.d.ts +1 -1
  248. package/dist/hooks/use-computer-input-posture.d.ts +13 -0
  249. package/dist/hooks/use-computer-session.d.ts +2 -0
  250. package/dist/hooks/use-connect.d.ts +22 -0
  251. package/dist/hooks/use-desktop-stream.d.ts +1 -1
  252. package/dist/hooks/use-file-attachments.d.ts +21 -2
  253. package/dist/hooks/use-machine-chip.d.ts +3 -3
  254. package/dist/hooks/use-machines.d.ts +19 -1
  255. package/dist/hooks/use-sandbox-files.d.ts +5 -1
  256. package/dist/hooks/use-session-background-commands.d.ts +14 -0
  257. package/dist/hooks/use-session-events.d.ts +30 -8
  258. package/dist/hooks/use-session-lineage.d.ts +4 -0
  259. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  260. package/dist/hooks/use-session.d.ts +6 -0
  261. package/dist/hooks/use-turn-queue.d.ts +12 -0
  262. package/dist/hooks/use-usage.d.ts +40 -0
  263. package/dist/hooks/use-workspace-edit.d.ts +4 -0
  264. package/dist/hooks/use-workspace-sessions.d.ts +27 -5
  265. package/dist/human-input.d.ts +3 -1
  266. package/dist/identity-link-accounts.d.ts +11 -0
  267. package/dist/identity-link-consent.d.ts +18 -0
  268. package/dist/index.d.ts +60 -17
  269. package/dist/index.js +2237 -6200
  270. package/dist/index.js.map +1 -1
  271. package/dist/interaction.d.ts +1 -1
  272. package/dist/interaction.js +35 -184
  273. package/dist/interaction.js.map +1 -1
  274. package/dist/latest-question-755OW5DS.js +117 -0
  275. package/dist/latest-question-755OW5DS.js.map +1 -0
  276. package/dist/lib/error-message.d.ts +7 -0
  277. package/dist/lib/format.d.ts +36 -2
  278. package/dist/lib/host-theme.d.ts +70 -0
  279. package/dist/lib/interaction-errors.d.ts +6 -0
  280. package/dist/lib/menu-styles.d.ts +22 -0
  281. package/dist/lib/pierre-diffs-loader.d.ts +20 -0
  282. package/dist/lib/workbench-peers.d.ts +36 -0
  283. package/dist/lib/xterm-theme.d.ts +7 -5
  284. package/dist/machines.d.ts +4 -2
  285. package/dist/machines.js +14 -4
  286. package/dist/markdown-table-layout-QXSLK4BY.js +106 -0
  287. package/dist/markdown-table-layout-QXSLK4BY.js.map +1 -0
  288. package/dist/mcp-connection-status.d.ts +14 -0
  289. package/dist/model-policy-picker-menu-PWRI7R2X.js +14 -0
  290. package/dist/model-policy-picker-menu-PWRI7R2X.js.map +1 -0
  291. package/dist/model-policy.d.ts +19 -3
  292. package/dist/model-policy.js +9 -1
  293. package/dist/older-history.d.ts +30 -0
  294. package/dist/platform-activity-row-627BFZ46.js +182 -0
  295. package/dist/platform-activity-row-627BFZ46.js.map +1 -0
  296. package/dist/plugin-details.d.ts +14 -0
  297. package/dist/plugin-discovery.d.ts +25 -0
  298. package/dist/provider.d.ts +13 -1
  299. package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-ZHFI5JBP.js} +131 -44
  300. package/dist/queue-surface-implementation-ZHFI5JBP.js.map +1 -0
  301. package/dist/realtime/realtime-control.d.ts +10 -3
  302. package/dist/realtime.js +229 -83
  303. package/dist/realtime.js.map +1 -1
  304. package/dist/sandbox-files-BBC47SLD.js +20 -0
  305. package/dist/sandbox-files-BBC47SLD.js.map +1 -0
  306. package/dist/session-auth-recommendation.d.ts +5 -0
  307. package/dist/session-capability-policy.d.ts +12 -0
  308. package/dist/session-ui.d.ts +30 -2
  309. package/dist/session-ui.js +85 -7
  310. package/dist/session-ui.js.map +1 -1
  311. package/dist/session.d.ts +8 -2
  312. package/dist/session.js +30 -19
  313. package/dist/sites-ui.d.ts +34 -0
  314. package/dist/sites.d.ts +3 -0
  315. package/dist/sites.js +11 -0
  316. package/dist/sites.js.map +1 -0
  317. package/dist/skill-discovery.d.ts +47 -0
  318. package/dist/terminal.d.ts +8 -0
  319. package/dist/terminal.js +31 -0
  320. package/dist/terminal.js.map +1 -0
  321. package/dist/timeline/activity-rail.d.ts +11 -2
  322. package/dist/timeline/allowance-exhausted-row.d.ts +24 -0
  323. package/dist/timeline/compaction-copy.d.ts +10 -0
  324. package/dist/timeline/entrance.d.ts +13 -1
  325. package/dist/timeline/genie-loading.d.ts +34 -0
  326. package/dist/timeline/index.d.ts +6 -2
  327. package/dist/timeline/knowledge-receipt.d.ts +17 -0
  328. package/dist/timeline/platform-activity-row.d.ts +14 -0
  329. package/dist/timeline/presented-image.d.ts +6 -0
  330. package/dist/timeline/projection.d.ts +31 -2
  331. package/dist/timeline/registry.d.ts +2 -0
  332. package/dist/timeline/retained-image.d.ts +18 -0
  333. package/dist/timeline/rolling-activity.d.ts +11 -0
  334. package/dist/timeline/screenshot-lightbox.d.ts +16 -1
  335. package/dist/timeline/shared.d.ts +8 -3
  336. package/dist/timeline/startup-preference.d.ts +3 -0
  337. package/dist/timeline/startup-timings.d.ts +4 -0
  338. package/dist/timeline/tool-display-name.d.ts +2 -1
  339. package/dist/timeline/turn-summary.d.ts +32 -3
  340. package/dist/timeline/types.d.ts +141 -7
  341. package/dist/timeline-annotation-markers-GNDHTGBD.js +165 -0
  342. package/dist/timeline-annotation-markers-GNDHTGBD.js.map +1 -0
  343. package/dist/timeline-annotation-selection-DMLMM2TX.js +241 -0
  344. package/dist/timeline-annotation-selection-DMLMM2TX.js.map +1 -0
  345. package/dist/timeline-annotations-dialog-ELRDBHTS.js +273 -0
  346. package/dist/timeline-annotations-dialog-ELRDBHTS.js.map +1 -0
  347. package/dist/types/machines.d.ts +2 -2
  348. package/dist/usage/allowance-copy.d.ts +25 -0
  349. package/dist/usage/summary.d.ts +35 -0
  350. package/dist/usage.d.ts +13 -0
  351. package/dist/usage.js +1074 -0
  352. package/dist/usage.js.map +1 -0
  353. package/package.json +44 -4
  354. package/src/accounts.tsx +1072 -0
  355. package/src/approvals.ts +8 -1
  356. package/src/artifacts.ts +42 -0
  357. package/src/assets/README.md +8 -0
  358. package/src/assets/gmail.ico +0 -0
  359. package/src/assets/outlook.ico +0 -0
  360. package/src/capability-catalog-row.tsx +94 -0
  361. package/src/capability-logo-fallback.ts +22 -0
  362. package/src/client.ts +22 -13
  363. package/src/commands/registry.ts +18 -15
  364. package/src/commands/types.ts +2 -0
  365. package/src/components/anthropic-mark.tsx +10 -0
  366. package/src/components/approval-surface.tsx +171 -37
  367. package/src/components/artifacts/artifact-chrome.tsx +413 -0
  368. package/src/components/artifacts/artifact-sandbox.tsx +185 -0
  369. package/src/components/artifacts/artifact-surface.tsx +39 -19
  370. package/src/components/artifacts/chat-interactive-block.tsx +260 -0
  371. package/src/components/artifacts/deferred-chat-media.tsx +67 -0
  372. package/src/components/artifacts/document-editor.tsx +141 -16
  373. package/src/components/artifacts/editable-artifact-ui.tsx +4 -2
  374. package/src/components/artifacts/editable-artifact-view.tsx +187 -0
  375. package/src/components/artifacts/editable-artifact-workbench.tsx +5 -2
  376. package/src/components/artifacts/editable-document.tsx +37 -36
  377. package/src/components/artifacts/editable-presentation.tsx +36 -18
  378. package/src/components/artifacts/editable-spreadsheet.tsx +5 -2
  379. package/src/components/artifacts/index.ts +44 -0
  380. package/src/components/artifacts/inline-html-document.ts +24 -0
  381. package/src/components/artifacts/latest-task-runner.ts +49 -0
  382. package/src/components/artifacts/presentation-editor.tsx +323 -67
  383. package/src/components/artifacts/published-html-artifact-frame.tsx +338 -5
  384. package/src/components/artifacts/session-artifact-viewer.tsx +376 -0
  385. package/src/components/artifacts/site-view.tsx +116 -0
  386. package/src/components/artifacts/spreadsheet-canvas.ts +5 -10
  387. package/src/components/artifacts/spreadsheet-general-display.ts +43 -0
  388. package/src/components/artifacts/spreadsheet-grid.tsx +12 -13
  389. package/src/components/artifacts/visualization-assets/base.css +1015 -0
  390. package/src/components/artifacts/visualization-assets/helpers.html +349 -0
  391. package/src/components/artifacts/visualization-assets.gen.ts +3 -0
  392. package/src/components/browser-select-control.tsx +312 -0
  393. package/src/components/browser-viewer.tsx +1054 -314
  394. package/src/components/chat-composer.tsx +131 -85
  395. package/src/components/child-session-link.tsx +27 -0
  396. package/src/components/claude-mark.tsx +10 -0
  397. package/src/components/code-editor.tsx +13 -16
  398. package/src/components/composer-transcription-control.tsx +7 -5
  399. package/src/components/composer.tsx +376 -49
  400. package/src/components/computer-viewer.tsx +1041 -329
  401. package/src/components/copy-button.tsx +13 -5
  402. package/src/components/desktop-viewer.tsx +1 -1
  403. package/src/components/enrollment-consent.tsx +65 -7
  404. package/src/components/enrollment-device-flow.tsx +1 -1
  405. package/src/components/file-browser.tsx +158 -34
  406. package/src/components/fleet-tile.tsx +8 -16
  407. package/src/components/generated-video-player.tsx +4 -2
  408. package/src/components/grok-mark.tsx +33 -0
  409. package/src/components/human-input-form.tsx +455 -114
  410. package/src/components/human-input-surface.tsx +18 -7
  411. package/src/components/interaction-intervention-banner.tsx +2 -2
  412. package/src/components/machine-card.tsx +119 -4
  413. package/src/components/machine-dock-bar.tsx +1 -1
  414. package/src/components/machine-input-display.ts +23 -2
  415. package/src/components/machines/machine-detail.tsx +52 -2
  416. package/src/components/machines/machine-operation-policy.tsx +179 -0
  417. package/src/components/machines-dashboard.tsx +8 -17
  418. package/src/components/markdown-table-layout.ts +132 -0
  419. package/src/components/markdown.tsx +473 -44
  420. package/src/components/message-timeline.tsx +2498 -663
  421. package/src/components/model-mark.tsx +90 -0
  422. package/src/components/model-picker.tsx +15 -4
  423. package/src/components/model-policy-picker-menu.tsx +424 -0
  424. package/src/components/model-policy-picker.tsx +337 -381
  425. package/src/components/open-geni-chat.tsx +319 -0
  426. package/src/components/open-geni-links.tsx +179 -0
  427. package/src/components/openrouter-mark.tsx +13 -0
  428. package/src/components/pierre-diff.tsx +41 -16
  429. package/src/components/pierre-file.tsx +36 -11
  430. package/src/components/preview-loading.tsx +154 -0
  431. package/src/components/queue-item-content.ts +14 -0
  432. package/src/components/queue-surface-implementation.tsx +132 -42
  433. package/src/components/queue-surface-state.tsx +3 -1
  434. package/src/components/sandbox-files.tsx +267 -35
  435. package/src/components/sandbox-terminal.tsx +48 -11
  436. package/src/components/sandbox-workspace.tsx +337 -135
  437. package/src/components/session-capability-frame.tsx +192 -0
  438. package/src/components/session-chrome.tsx +1027 -328
  439. package/src/components/session-commands-panel.tsx +126 -0
  440. package/src/components/session-commands.tsx +18 -0
  441. package/src/components/session-connection-request.tsx +97 -0
  442. package/src/components/session-conversation.tsx +512 -0
  443. package/src/components/session-list.tsx +250 -0
  444. package/src/components/session-mcp-capability-card.tsx +680 -0
  445. package/src/components/session-status.tsx +1 -1
  446. package/src/components/timeline-anchor.tsx +119 -0
  447. package/src/components/timeline-annotation-chrome.tsx +196 -0
  448. package/src/components/timeline-annotation-layout.ts +268 -0
  449. package/src/components/timeline-annotation-markers.tsx +185 -0
  450. package/src/components/timeline-annotation-reveal-context.ts +9 -0
  451. package/src/components/timeline-annotation-selection.tsx +106 -54
  452. package/src/components/timeline-annotation-shared.ts +245 -0
  453. package/src/components/timeline-annotations-dialog.tsx +185 -128
  454. package/src/components/timeline-annotations.tsx +189 -45
  455. package/src/components/timeline-reading-progress.ts +80 -0
  456. package/src/components/timeline-search.tsx +168 -0
  457. package/src/components/timeline-settlement.ts +60 -0
  458. package/src/components/tip-follow.ts +70 -7
  459. package/src/components/tooltip.tsx +38 -5
  460. package/src/components/usage-limit-notice.tsx +179 -0
  461. package/src/components/usage-member-list.tsx +792 -0
  462. package/src/components/usage-meter.tsx +382 -0
  463. package/src/components/use-viewer-menu-dismiss.ts +33 -0
  464. package/src/components/user-message-body.tsx +194 -49
  465. package/src/components/workspace-dock.tsx +141 -175
  466. package/src/composer.ts +6 -1
  467. package/src/connect-account-identity.tsx +34 -0
  468. package/src/connect-accounts.tsx +233 -0
  469. package/src/connect-chooser.tsx +336 -0
  470. package/src/connect-panel.tsx +99 -0
  471. package/src/connect-setup.tsx +307 -0
  472. package/src/connect.ts +60 -0
  473. package/src/connection-assets.d.ts +4 -0
  474. package/src/connection-catalog.tsx +162 -0
  475. package/src/connection-discovery.tsx +235 -0
  476. package/src/connection-installed.tsx +51 -0
  477. package/src/connection-logo.tsx +37 -0
  478. package/src/connection-service-logo.tsx +55 -0
  479. package/src/connection-service-presentation.ts +46 -0
  480. package/src/connection-skeleton.tsx +16 -0
  481. package/src/connection-type-picker.tsx +36 -0
  482. package/src/conversation-timeline.ts +92 -0
  483. package/src/desktop.ts +24 -0
  484. package/src/device-authorization.tsx +98 -0
  485. package/src/diffs.ts +16 -0
  486. package/src/discovery-cache.ts +42 -0
  487. package/src/editor.ts +13 -0
  488. package/src/embedded-session-client.ts +104 -0
  489. package/src/file-node-visibility.ts +66 -0
  490. package/src/hooks/channel-cache.ts +104 -0
  491. package/src/hooks/internal.ts +3 -2
  492. package/src/hooks/latest-question.ts +164 -0
  493. package/src/hooks/shared-machines-list.ts +239 -0
  494. package/src/hooks/use-attached-browsers.ts +5 -1
  495. package/src/hooks/use-available-models.ts +20 -14
  496. package/src/hooks/use-browser-frame-stream.ts +25 -3
  497. package/src/hooks/use-browser-session.ts +271 -71
  498. package/src/hooks/use-browser-sessions.ts +7 -1
  499. package/src/hooks/use-channels.ts +32 -10
  500. package/src/hooks/use-codex-accounts.ts +78 -21
  501. package/src/hooks/use-composer.ts +1034 -320
  502. package/src/hooks/use-computer-frame-stream.ts +39 -16
  503. package/src/hooks/use-computer-input-posture.ts +94 -0
  504. package/src/hooks/use-computer-session.ts +110 -32
  505. package/src/hooks/use-computer-sessions.ts +8 -1
  506. package/src/hooks/use-connect.ts +23 -0
  507. package/src/hooks/use-desktop-stream.ts +19 -28
  508. package/src/hooks/use-file-attachments.ts +118 -10
  509. package/src/hooks/use-goal.ts +36 -9
  510. package/src/hooks/use-human-input.ts +31 -4
  511. package/src/hooks/use-last-started-turn-policy.ts +15 -13
  512. package/src/hooks/use-machine-chip.ts +23 -6
  513. package/src/hooks/use-machines.ts +104 -6
  514. package/src/hooks/use-sandbox-files.ts +253 -150
  515. package/src/hooks/use-sandbox-git.ts +58 -29
  516. package/src/hooks/use-session-background-commands.ts +67 -0
  517. package/src/hooks/use-session-capabilities.ts +21 -2
  518. package/src/hooks/use-session-events.ts +759 -433
  519. package/src/hooks/use-session-lineage.ts +28 -6
  520. package/src/hooks/use-session-mcp-approval-policy.ts +13 -5
  521. package/src/hooks/use-session.ts +75 -16
  522. package/src/hooks/use-slash-commands.ts +6 -11
  523. package/src/hooks/use-turn-queue.ts +113 -13
  524. package/src/hooks/use-usage.ts +100 -0
  525. package/src/hooks/use-voice-input.ts +12 -1
  526. package/src/hooks/use-workspace-capture.ts +3 -2
  527. package/src/hooks/use-workspace-edit.ts +23 -5
  528. package/src/hooks/use-workspace-sessions.ts +99 -11
  529. package/src/human-input.ts +12 -1
  530. package/src/identity-link-accounts.tsx +148 -0
  531. package/src/identity-link-consent.tsx +192 -0
  532. package/src/index.ts +157 -12
  533. package/src/interaction.ts +1 -0
  534. package/src/lib/error-message.ts +37 -0
  535. package/src/lib/format.ts +150 -14
  536. package/src/lib/host-theme.ts +511 -0
  537. package/src/lib/interaction-errors.ts +33 -0
  538. package/src/lib/menu-styles.ts +44 -0
  539. package/src/lib/pierre-diffs-loader.ts +53 -0
  540. package/src/lib/workbench-peers.ts +62 -0
  541. package/src/lib/xterm-theme.ts +23 -21
  542. package/src/machines.ts +9 -1
  543. package/src/mcp-connection-status.ts +35 -0
  544. package/src/model-policy.ts +223 -69
  545. package/src/older-history.ts +87 -0
  546. package/src/plugin-details.tsx +185 -0
  547. package/src/plugin-discovery.tsx +196 -0
  548. package/src/provider.tsx +55 -14
  549. package/src/realtime/dropdown-menu.tsx +19 -12
  550. package/src/realtime/realtime-control.tsx +274 -60
  551. package/src/session-auth-recommendation.ts +48 -0
  552. package/src/session-capability-policy.ts +83 -0
  553. package/src/session-context.ts +1 -0
  554. package/src/session-ui.ts +66 -2
  555. package/src/session.ts +18 -1
  556. package/src/sites-ui.tsx +402 -0
  557. package/src/sites.ts +3 -0
  558. package/src/skill-discovery.tsx +208 -0
  559. package/src/terminal.ts +26 -0
  560. package/src/timeline/activity-rail.tsx +171 -80
  561. package/src/timeline/allowance-exhausted-row.tsx +86 -0
  562. package/src/timeline/compaction-copy.ts +42 -0
  563. package/src/timeline/entrance.tsx +28 -3
  564. package/src/timeline/fleet-decision-projection.ts +2 -1
  565. package/src/timeline/fleet-decision-row.tsx +1 -0
  566. package/src/timeline/genie-loading.tsx +136 -0
  567. package/src/timeline/index.ts +11 -0
  568. package/src/timeline/knowledge-receipt.tsx +100 -0
  569. package/src/timeline/parsers.ts +2 -2
  570. package/src/timeline/platform-activity-row.tsx +216 -0
  571. package/src/timeline/presented-image.ts +49 -0
  572. package/src/timeline/projection.ts +1600 -84
  573. package/src/timeline/registry.ts +15 -3
  574. package/src/timeline/retained-image.ts +97 -0
  575. package/src/timeline/rolling-activity.tsx +92 -0
  576. package/src/timeline/screenshot-lightbox.tsx +115 -17
  577. package/src/timeline/shared.tsx +47 -15
  578. package/src/timeline/startup-preference.ts +33 -0
  579. package/src/timeline/startup-timings.tsx +145 -0
  580. package/src/timeline/tool-display-name.ts +8 -6
  581. package/src/timeline/tool-renderers.tsx +497 -128
  582. package/src/timeline/turn-summary.tsx +208 -44
  583. package/src/timeline/types.ts +186 -7
  584. package/src/types/machines.ts +10 -0
  585. package/src/usage/allowance-copy.ts +64 -0
  586. package/src/usage/summary.ts +93 -0
  587. package/src/usage.ts +50 -0
  588. package/styles/compiled.css +2098 -757
  589. package/styles/connect.css +284 -0
  590. package/styles/connect.d.ts +1 -0
  591. package/styles/effective-tokens.css +1 -0
  592. package/styles/index.css +201 -9
  593. package/styles/responsive.css +27 -1
  594. package/styles/session-capability.css +44 -0
  595. package/styles/tokens.css +135 -65
  596. package/dist/chunk-5UQPNLIO.js.map +0 -1
  597. package/dist/chunk-6KLX2RH5.js.map +0 -1
  598. package/dist/chunk-7WCLAEFV.js.map +0 -1
  599. package/dist/chunk-B6WPZSTT.js.map +0 -1
  600. package/dist/chunk-FK6VG4LL.js +0 -90
  601. package/dist/chunk-FK6VG4LL.js.map +0 -1
  602. package/dist/chunk-HA46CYQK.js.map +0 -1
  603. package/dist/chunk-HUIKIDM5.js.map +0 -1
  604. package/dist/chunk-IGC6A2ZI.js.map +0 -1
  605. package/dist/chunk-KCGXPP5V.js.map +0 -1
  606. package/dist/chunk-KZ73RL76.js.map +0 -1
  607. package/dist/chunk-LAKLF4PA.js +0 -141
  608. package/dist/chunk-LAKLF4PA.js.map +0 -1
  609. package/dist/chunk-LLOLUHVG.js.map +0 -1
  610. package/dist/chunk-MKPIAGVL.js +0 -465
  611. package/dist/chunk-MKPIAGVL.js.map +0 -1
  612. package/dist/chunk-PEI6RANI.js.map +0 -1
  613. package/dist/chunk-R3AZOAB5.js.map +0 -1
  614. package/dist/chunk-WSK7G5LE.js +0 -170
  615. package/dist/chunk-WSK7G5LE.js.map +0 -1
  616. package/dist/chunk-XBZWOM2K.js +0 -218
  617. package/dist/chunk-XBZWOM2K.js.map +0 -1
  618. package/dist/chunk-Y7OEDUJ5.js +0 -8810
  619. package/dist/chunk-Y7OEDUJ5.js.map +0 -1
  620. package/dist/fleet-decision-row-GGCAT4E4.js.map +0 -1
  621. package/dist/hooks/use-packs.d.ts +0 -32
  622. package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
  623. package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
  624. package/dist/timeline-annotation-selection-3L7LHGG2.js +0 -187
  625. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +0 -1
  626. package/dist/timeline-annotations-dialog-U7EVLR75.js +0 -196
  627. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +0 -1
  628. package/src/hooks/use-packs.ts +0 -157
  629. /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-GTR5XQ7X.js.map} +0 -0
@@ -4,17 +4,38 @@ import {
4
4
  type DraftTimelineAnnotation,
5
5
  type EffectiveControlResumeOption,
6
6
  type EffectiveSessionControl,
7
- type OpenGeniApiError,
7
+ type LatencyMode,
8
+ OpenGeniApiError,
9
+ type ReasoningEffort,
8
10
  type ResourceRef,
9
11
  type SaveComposerDraftRequest,
10
12
  type SendMessageInput,
11
13
  type SessionEvent,
14
+ type SubmitComposerDraftResponse,
12
15
  } from "@opengeni/sdk";
13
16
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
14
17
  import { useEmbeddedSession, type EmbeddedSessionClientOverride } from "../session-context";
18
+ import {
19
+ COMPOSER_MODEL_UNAVAILABLE_MESSAGE,
20
+ ComposerReconciliationRequiredError,
21
+ ComposerStateError,
22
+ ComposerWorkspaceControlUnavailableError,
23
+ composerSubmissionCanRetry,
24
+ composerSubmissionErrorMessage,
25
+ } from "../lib/format";
26
+ import { normalizeError, useErrorMessage } from "../lib/error-message";
15
27
  import { useSessionEventTrigger, type SessionEventFeedOptions } from "./internal";
16
28
 
17
- export type ComposerSendExtras = Omit<SendMessageInput, "text" | "clientEventId" | "annotations">;
29
+ export type ComposerPolicy = {
30
+ model: string;
31
+ reasoningEffort: ReasoningEffort;
32
+ latencyMode: LatencyMode;
33
+ };
34
+
35
+ export type ComposerSendExtras = Omit<
36
+ SendMessageInput,
37
+ "text" | "clientEventId" | "annotations" | "model" | "reasoningEffort" | "latencyMode"
38
+ >;
18
39
 
19
40
  export type UseComposerOptions = EmbeddedSessionClientOverride &
20
41
  SessionEventFeedOptions & {
@@ -22,10 +43,13 @@ export type UseComposerOptions = EmbeddedSessionClientOverride &
22
43
  onSubmitted?: ((text: string, input: SendMessageInput) => void) | undefined;
23
44
  /** Called with the exact accepted wire input after a successful send. */
24
45
  onSent?: ((text: string, input: SendMessageInput) => void) | undefined;
46
+ /** Called with the exact wire input after a delivery failure. */
47
+ onDeliveryError?:
48
+ | ((error: Error, input: SendMessageInput, delivery: "send" | "steer") => void)
49
+ | undefined;
25
50
  /**
26
- * Extra message fields (resources, tools, model, reasoningEffort, latencyMode) merged
27
- * into every send. A function is evaluated at send time so it can read the
28
- * surrounding UI state (attachment pickers, model selectors, ...).
51
+ * Non-policy message fields merged into every send. Durable policy belongs
52
+ * to this composer draft; attachments and credentials may remain host-owned.
29
53
  */
30
54
  sendExtras?: ComposerSendExtras | (() => ComposerSendExtras) | undefined;
31
55
  /**
@@ -36,16 +60,19 @@ export type UseComposerOptions = EmbeddedSessionClientOverride &
36
60
  sendBlocked?: (() => boolean) | undefined;
37
61
  /** Latest server-derived workstream control; bound into Send/Steer OCC. */
38
62
  effectiveControl?: EffectiveSessionControl | null | undefined;
39
- /** Apply durable model/tool/reasoning settings in the host's controlled UI. */
40
- onDraftApplied?: ((draft: ComposerDraft) => void) | undefined;
63
+ /** Immediate user-visible destination for an ordinary Send before admission replies. */
64
+ sendDestination?: (() => "chat" | "queue") | undefined;
41
65
  /** Disable remote composer-draft reads and writes for embedded hosts. */
42
66
  draftPersistence?: "durable" | "disabled" | undefined;
67
+ /** Required explicit authority when durable draft persistence is disabled. */
68
+ initialPolicy?: ComposerPolicy | undefined;
43
69
  };
44
70
 
45
71
  type ComposerDraftShadow = {
46
72
  text: string;
47
73
  resources: ResourceRef[];
48
74
  annotations: DraftTimelineAnnotation[];
75
+ policy?: ComposerPolicy | undefined;
49
76
  };
50
77
 
51
78
  type PendingComposerOperation = {
@@ -68,12 +95,21 @@ type StoredPendingComposerOperation = Omit<PendingComposerOperation, "input" | "
68
95
 
69
96
  export type ComposerOptimisticMessage = {
70
97
  clientEventId: string;
98
+ delivery: "send" | "steer";
99
+ destination: "chat" | "queue";
71
100
  text: string;
72
101
  annotations: DraftTimelineAnnotation[];
73
102
  resources: ResourceRef[];
74
103
  occurredAt: string;
75
104
  state: "sending" | "queued" | "failed";
105
+ turnId?: string | null | undefined;
106
+ triggerEventId?: string | null | undefined;
107
+ appliedQueueVersion?: number | null | undefined;
76
108
  error?: string | undefined;
109
+ /** Bounded support reference retained independently of display copy across remounts. */
110
+ correlationId?: string | undefined;
111
+ /** False for a definitive refusal that requires changing payment or allowance settings. */
112
+ retryable?: boolean | undefined;
77
113
  outcomeUnknown?: boolean | undefined;
78
114
  };
79
115
 
@@ -92,7 +128,6 @@ type StoredOptimisticSendOperation = Omit<OptimisticSendOperation, "input" | "ca
92
128
 
93
129
  const PENDING_COMPOSER_STORAGE_PREFIX = "opengeni.pending-composer.v1:";
94
130
  const OPTIMISTIC_SEND_STORAGE_PREFIX = "opengeni.optimistic-sends.v1:";
95
-
96
131
  // A remount must not manufacture a new operation while the previous mutation
97
132
  // is still outcome-unknown. Keep only non-credential request fields here; the
98
133
  // mounted hook retains the exact input, including any credential updates. The
@@ -151,6 +186,8 @@ function restoreOptimisticSendOperations(key: string | null): OptimisticSendOper
151
186
  }
152
187
  return stored.map(({ hasMcpCredentialUpdates, ...operation }) => ({
153
188
  ...operation,
189
+ delivery: "send",
190
+ destination: operation.destination === "queue" ? "queue" : "chat",
154
191
  newerShadow: operation.newerShadow ?? { text: "", resources: [], annotations: [] },
155
192
  state:
156
193
  operation.state === "sending" || operation.state === "queued" ? "sending" : operation.state,
@@ -158,15 +195,43 @@ function restoreOptimisticSendOperations(key: string | null): OptimisticSendOper
158
195
  operation.state === "sending" || operation.state === "queued"
159
196
  ? true
160
197
  : operation.outcomeUnknown,
161
- error:
162
- operation.state === "sending"
163
- ? "Delivery was interrupted; retry to reconcile this message."
164
- : operation.error,
198
+ correlationId: restoredCorrelationId(operation.correlationId),
199
+ error: restoredErrorReference(
200
+ operation.correlationId,
201
+ hasMcpCredentialUpdates && (operation.state === "sending" || operation.outcomeUnknown)
202
+ ? composerSubmissionErrorMessage(new ComposerReconciliationRequiredError())
203
+ : operation.state === "sending"
204
+ ? "Delivery was interrupted; retry to reconcile this message."
205
+ : operation.outcomeUnknown
206
+ ? "The request could not be confirmed. Check its status before retrying."
207
+ : operation.error
208
+ ? composerSubmissionErrorMessage(new Error(operation.error))
209
+ : undefined,
210
+ ),
211
+ retryable:
212
+ operation.state === "sending" || operation.outcomeUnknown
213
+ ? true
214
+ : (operation.retryable ??
215
+ (operation.error ? composerSubmissionCanRetry(new Error(operation.error)) : true)),
165
216
  input: operation.input,
166
217
  canRetry: !hasMcpCredentialUpdates,
167
218
  }));
168
219
  }
169
220
 
221
+ function restoredCorrelationId(value: unknown): string | undefined {
222
+ return typeof value === "string" && /^[\w.:-]{1,128}$/u.test(value) ? value : undefined;
223
+ }
224
+
225
+ function restoredErrorReference(
226
+ reference: unknown,
227
+ message: string | undefined,
228
+ ): string | undefined {
229
+ const correlationId = restoredCorrelationId(reference);
230
+ return message && correlationId && message !== COMPOSER_MODEL_UNAVAILABLE_MESSAGE
231
+ ? `${message} Reference: ${correlationId}.`
232
+ : message;
233
+ }
234
+
170
235
  function rememberOptimisticSendOperations(
171
236
  key: string | null,
172
237
  operations: OptimisticSendOperation[],
@@ -183,6 +248,15 @@ function rememberOptimisticSendOperations(
183
248
  hasMcpCredentialUpdates: input.mcpCredentialUpdates !== undefined,
184
249
  } satisfies StoredOptimisticSendOperation;
185
250
  });
251
+ if (stored.length === 0) {
252
+ optimisticSendOperations.delete(key);
253
+ try {
254
+ pendingComposerStorage()?.removeItem(optimisticSendStorageKey(key));
255
+ } catch {
256
+ // Best effort; the in-memory entry is already gone.
257
+ }
258
+ return;
259
+ }
186
260
  optimisticSendOperations.set(key, stored);
187
261
  try {
188
262
  pendingComposerStorage()?.setItem(optimisticSendStorageKey(key), JSON.stringify(stored));
@@ -344,6 +418,7 @@ function rememberPendingComposerOperation(
344
418
  text: operation.newerShadow.text,
345
419
  resources: [...operation.newerShadow.resources],
346
420
  annotations: cloneAnnotations(operation.newerShadow.annotations),
421
+ ...(operation.newerShadow.policy ? { policy: { ...operation.newerShadow.policy } } : {}),
347
422
  },
348
423
  } satisfies StoredPendingComposerOperation;
349
424
  pendingComposerOperations.set(key, stored);
@@ -390,6 +465,8 @@ export type ComposerState = {
390
465
  /** Newly captured annotation the review surface should focus. */
391
466
  annotationReviewTargetId?: string | null | undefined;
392
467
  clearAnnotationReviewTarget?: (() => void) | undefined;
468
+ /** Focus an annotation, or the first incomplete note when omitted. */
469
+ requestAnnotationReview?: ((id?: string) => void) | undefined;
393
470
  /** Read the current draft synchronously before a destructive replacement. */
394
471
  hasDraftContent: () => boolean;
395
472
  /** Append the draft behind prompts already visible in the queue. */
@@ -397,6 +474,8 @@ export type ComposerState = {
397
474
  /** Locally acknowledged ordinary sends awaiting durable timeline reconciliation. */
398
475
  optimisticMessages?: ComposerOptimisticMessage[] | undefined;
399
476
  retryOptimisticMessage?: ((clientEventId: string) => void) | undefined;
477
+ /** Move a definitively refused message into an empty composer without resending it. */
478
+ restoreOptimisticMessage?: ((clientEventId: string) => void) | undefined;
400
479
  removeOptimisticMessage?: ((clientEventId: string) => void) | undefined;
401
480
  /** Supersede current direction with the draft. */
402
481
  steer: (text?: string) => Promise<boolean>;
@@ -412,11 +491,18 @@ export type ComposerState = {
412
491
  resume: (reason?: string) => Promise<void>;
413
492
  resumeScope: (option: EffectiveControlResumeOption) => Promise<void>;
414
493
  resuming: boolean;
494
+ /** Mutation-confirmed control while the streamed queue projection catches up. */
495
+ effectiveControl?: EffectiveSessionControl | null | undefined;
415
496
  draft: ComposerDraft | null;
416
497
  draftRevision: number;
417
498
  draftLoading: boolean;
418
499
  draftSaving: boolean;
419
500
  draftConflict: Error | null;
501
+ /** Exact policy owned by this actor/session composer. Null while hydrating. */
502
+ policy?: ComposerPolicy | null | undefined;
503
+ setModel?: ((model: string) => void) | undefined;
504
+ setReasoningEffort?: ((effort: ReasoningEffort) => void) | undefined;
505
+ setLatencyMode?: ((mode: LatencyMode) => void) | undefined;
420
506
  /** Whether this controller owns a durable server-side draft. */
421
507
  draftPersistence?: "durable" | "disabled" | undefined;
422
508
  /** Apply an atomic queue Edit checkout without a second read. */
@@ -429,19 +515,23 @@ export type ComposerState = {
429
515
  clearError: () => void;
430
516
  };
431
517
 
518
+ export type ComposerControllerState = ComposerState & {
519
+ policy: ComposerPolicy | null;
520
+ setModel: (model: string) => void;
521
+ setReasoningEffort: (effort: ReasoningEffort) => void;
522
+ setLatencyMode: (mode: LatencyMode) => void;
523
+ };
524
+
432
525
  export type ComposerSteeringState = {
433
- phase: "submitting" | "accepted";
526
+ phase: "submitting" | "accepted" | "failed";
434
527
  text: string;
435
528
  clientEventId: string | null;
436
529
  triggerEventId: string | null;
437
530
  turnId: string | null;
438
531
  /** True only when the accepted Steer durably interrupted a live attempt. */
439
532
  stoppingPreviousAttempt?: boolean | undefined;
440
- };
441
-
442
- type ComposerControlStoppingState = {
443
- controlVersion: number;
444
- controlEtag: string;
533
+ error?: string | undefined;
534
+ outcomeUnknown?: boolean | undefined;
445
535
  };
446
536
 
447
537
  const STEERING_SETTLEMENT_EVENT_TYPES = new Set([
@@ -468,6 +558,32 @@ function steeringAcceptedEvent(
468
558
  );
469
559
  }
470
560
 
561
+ function promptTurnIdForTrigger(
562
+ triggerEventId: string | null | undefined,
563
+ events: readonly SessionEvent[],
564
+ ): string | null {
565
+ if (!triggerEventId) return null;
566
+ const queued = events.find((event) => {
567
+ if (event.type !== "turn.queued") return false;
568
+ const payload = event.payload;
569
+ return (
570
+ typeof payload === "object" &&
571
+ payload !== null &&
572
+ "triggerEventId" in payload &&
573
+ payload.triggerEventId === triggerEventId
574
+ );
575
+ });
576
+ if (!queued) return null;
577
+ if (queued.turnId) return queued.turnId;
578
+ const payload = queued.payload;
579
+ return typeof payload === "object" &&
580
+ payload !== null &&
581
+ "turnId" in payload &&
582
+ typeof payload.turnId === "string"
583
+ ? payload.turnId
584
+ : null;
585
+ }
586
+
471
587
  function steeringSettledByEvents(
472
588
  steering: ComposerSteeringState,
473
589
  events: readonly SessionEvent[],
@@ -489,6 +605,72 @@ function steeringSettledByEvents(
489
605
  });
490
606
  }
491
607
 
608
+ function reconcileOptimisticSendFromEvents(
609
+ operation: OptimisticSendOperation,
610
+ events: readonly SessionEvent[],
611
+ ): { operation: OptimisticSendOperation; admitted: boolean; settled: boolean } {
612
+ const accepted = events.find(
613
+ (event) => event.type === "user.message" && event.clientEventId === operation.clientEventId,
614
+ );
615
+ const triggerEventId = operation.triggerEventId ?? accepted?.id ?? null;
616
+ const turnId = operation.turnId ?? promptTurnIdForTrigger(triggerEventId, events);
617
+ const acceptedRouting = promptRoutingFromAcceptedEvent(accepted ?? null);
618
+ const destination =
619
+ acceptedRouting === "queued_for_execution"
620
+ ? "queue"
621
+ : acceptedRouting === "accepted_for_execution" || acceptedRouting === "accepted_for_steering"
622
+ ? "chat"
623
+ : operation.destination;
624
+ const needsAdmissionUpdate =
625
+ accepted !== undefined &&
626
+ (operation.state !== "queued" ||
627
+ operation.destination !== destination ||
628
+ operation.triggerEventId !== triggerEventId ||
629
+ operation.turnId !== turnId ||
630
+ operation.error !== undefined ||
631
+ operation.outcomeUnknown !== false);
632
+ const reconciled: OptimisticSendOperation = needsAdmissionUpdate
633
+ ? {
634
+ ...operation,
635
+ state: "queued",
636
+ destination,
637
+ triggerEventId,
638
+ turnId,
639
+ error: undefined,
640
+ outcomeUnknown: false,
641
+ }
642
+ : operation;
643
+ const withdrawnFromQueue = events.some((event) => {
644
+ if (event.type !== "session.queue.changed" || !turnId) return false;
645
+ const payload = event.payload;
646
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return false;
647
+ const record = payload as Record<string, unknown>;
648
+ return (
649
+ record.turnId === turnId && (record.operation === "edit" || record.operation === "delete")
650
+ );
651
+ });
652
+ return {
653
+ operation: reconciled,
654
+ admitted: accepted !== undefined,
655
+ // Edit/Delete atomically withdraw the queued turn. Keeping its optimistic
656
+ // send alive after that point lets its old `newerShadow` become the newest
657
+ // composer draft again when a later turn settles, duplicating submitted
658
+ // text. The queue mutation is the durable terminal fact for this send.
659
+ settled:
660
+ withdrawnFromQueue ||
661
+ steeringSettledByEvents(
662
+ {
663
+ phase: "accepted",
664
+ text: reconciled.text,
665
+ clientEventId: reconciled.clientEventId,
666
+ triggerEventId: reconciled.triggerEventId ?? null,
667
+ turnId: reconciled.turnId ?? null,
668
+ },
669
+ events,
670
+ ),
671
+ };
672
+ }
673
+
492
674
  /**
493
675
  * Draft + send + Pause/Resume state for the chat composer — the only
494
676
  * human-to-agent input surface. The draft survives a failed send (nothing is
@@ -498,9 +680,13 @@ function steeringSettledByEvents(
498
680
  export function useComposer(
499
681
  sessionId: string | null | undefined,
500
682
  options: UseComposerOptions = {},
501
- ): ComposerState {
683
+ ): ComposerControllerState {
502
684
  const { client, workspaceId, registerSessionReconciler } = useEmbeddedSession(options);
685
+ const formatError = useErrorMessage();
503
686
  const durableDrafts = options.draftPersistence !== "disabled";
687
+ if (!durableDrafts && !options.initialPolicy) {
688
+ throw new Error("useComposer requires initialPolicy when draft persistence is disabled");
689
+ }
504
690
  const targetKey = `${workspaceId}\u0000${sessionId ?? ""}\u0000${durableDrafts ? "durable" : "disabled"}`;
505
691
  const pendingOperationKey = pendingComposerOperationKey(workspaceId, sessionId);
506
692
  const initialPendingOperation = restorePendingComposerOperation(pendingOperationKey);
@@ -521,22 +707,29 @@ export function useComposer(
521
707
  const [steering, setSteering] = useState<ComposerSteeringState | null>(() =>
522
708
  initialPendingOperation?.delivery === "steer"
523
709
  ? {
524
- phase: "submitting",
710
+ phase: "failed",
525
711
  text: initialPendingOperation.input.text,
526
712
  clientEventId: initialPendingOperation.input.clientEventId ?? null,
527
713
  triggerEventId: null,
528
714
  turnId: null,
715
+ error: "Not confirmed. Retry to check the same direction change.",
716
+ outcomeUnknown: true,
529
717
  }
530
718
  : null,
531
719
  );
532
720
  const [pausing, setPausing] = useState(false);
533
- const [controlStopping, setControlStopping] = useState<ComposerControlStoppingState | null>(null);
721
+ const [acceptedControl, setAcceptedControl] = useState<EffectiveSessionControl | null>(null);
534
722
  const [resuming, setResuming] = useState(false);
535
723
  const [error, setError] = useState<Error | null>(null);
724
+ // Background draft reads recover independently of Send/Steer/control failures.
725
+ const [draftReadError, setDraftReadError] = useState<Error | null>(null);
536
726
  const [draft, setDraft] = useState<ComposerDraft | null>(null);
537
727
  const [draftLoading, setDraftLoading] = useState(Boolean(sessionId) && durableDrafts);
538
728
  const [draftSaving, setDraftSaving] = useState(false);
539
729
  const [draftConflict, setDraftConflict] = useState<Error | null>(null);
730
+ const [policy, setPolicy] = useState<ComposerPolicy | null>(
731
+ () => initialShadow?.policy ?? options.initialPolicy ?? null,
732
+ );
540
733
  const [restoredResources, setRestoredResources] = useState<ResourceRef[]>(
541
734
  () => initialShadow?.resources ?? [],
542
735
  );
@@ -546,29 +739,33 @@ export function useComposer(
546
739
  const optimisticCallbackIdsRef = useRef(new Set<string>());
547
740
  const steeringSettlementEventsRef = useRef<SessionEvent[]>([]);
548
741
  const steeringRef = useRef(steering);
742
+ const steeringOccurredAtRef = useRef(new Date().toISOString());
549
743
  const pendingClientEventId = useRef<string | null>(
550
744
  initialPendingOperation?.input.clientEventId ?? null,
551
745
  );
552
746
  const valueRef = useRef(initialShadow?.text ?? "");
553
747
  const annotationsRef = useRef<DraftTimelineAnnotation[]>(initialShadow?.annotations ?? []);
748
+ const policyRef = useRef<ComposerPolicy | null>(
749
+ initialShadow?.policy ?? options.initialPolicy ?? null,
750
+ );
554
751
  const draftRef = useRef<ComposerDraft | null>(null);
555
752
  const restoredResourcesRef = useRef<ResourceRef[]>(initialShadow?.resources ?? []);
556
753
  const localEditRevision = useRef(initialShadow ? 1 : 0);
557
754
  const targetGeneration = useRef(0);
558
755
  const draftReadGeneration = useRef(0);
756
+ const draftReadInFlightRef = useRef<string | null>(null);
757
+ const draftReadAbortRef = useRef<AbortController | null>(null);
758
+ const draftReadRetryRef = useRef<{
759
+ targetKey: string;
760
+ failures: number;
761
+ timer: ReturnType<typeof setTimeout> | null;
762
+ }>({ targetKey, failures: 0, timer: null });
559
763
  const lastSavedSignature = useRef<string | null>(null);
560
764
  const saveChain = useRef<Promise<void>>(Promise.resolve());
561
765
  const onSent = options.onSent;
562
766
  const onSubmitted = options.onSubmitted;
563
- const onDraftApplied = options.onDraftApplied;
564
- // Read through a ref so live session/policy projections can replace their
565
- // apply callback without invalidating the draft loader and re-running its
566
- // initial-load effect. Publish only committed callbacks: a suspended target
567
- // render must not retarget an in-flight read owned by the committed session.
568
- const onDraftAppliedRef = useRef(onDraftApplied);
569
- useLayoutEffect(() => {
570
- onDraftAppliedRef.current = onDraftApplied;
571
- }, [onDraftApplied]);
767
+ const onDeliveryErrorRef = useRef(options.onDeliveryError);
768
+ onDeliveryErrorRef.current = options.onDeliveryError;
572
769
  useLayoutEffect(() => {
573
770
  steeringRef.current = steering;
574
771
  }, [steering]);
@@ -576,8 +773,14 @@ export function useComposer(
576
773
  // callers passing inline functions) does not invalidate `send`.
577
774
  const sendExtrasRef = useRef(options.sendExtras);
578
775
  sendExtrasRef.current = options.sendExtras;
776
+ const sendDestinationRef = useRef(options.sendDestination);
777
+ sendDestinationRef.current = options.sendDestination;
579
778
  const sendBlockedRef = useRef(options.sendBlocked);
580
779
  sendBlockedRef.current = options.sendBlocked;
780
+ const effectiveControlRef = useRef<EffectiveSessionControl | null>(
781
+ acceptedControl ?? options.effectiveControl ?? null,
782
+ );
783
+ effectiveControlRef.current = acceptedControl ?? options.effectiveControl ?? null;
581
784
  const liveExtrasVersion = JSON.stringify(resolveSendExtras(options.sendExtras));
582
785
 
583
786
  // A composer is bound to one session: switching targets must not leak the
@@ -585,6 +788,12 @@ export function useComposer(
585
788
  const targetKeyRef = useRef(targetKey);
586
789
  useLayoutEffect(() => {
587
790
  if (targetKeyRef.current === targetKey) return;
791
+ if (draftReadRetryRef.current.timer !== null) {
792
+ clearTimeout(draftReadRetryRef.current.timer);
793
+ }
794
+ draftReadAbortRef.current?.abort();
795
+ draftReadAbortRef.current = null;
796
+ draftReadRetryRef.current = { targetKey, failures: 0, timer: null };
588
797
  targetKeyRef.current = targetKey;
589
798
  targetGeneration.current += 1;
590
799
  draftReadGeneration.current += 1;
@@ -600,6 +809,7 @@ export function useComposer(
600
809
  localEditRevision.current = shadow ? 1 : 0;
601
810
  valueRef.current = shadow?.text ?? "";
602
811
  annotationsRef.current = shadow?.annotations ?? [];
812
+ policyRef.current = shadow?.policy ?? options.initialPolicy ?? null;
603
813
  draftRef.current = null;
604
814
  restoredResourcesRef.current = shadow?.resources ?? [];
605
815
  lastSavedSignature.current = null;
@@ -609,30 +819,47 @@ export function useComposer(
609
819
  setStateTargetKey(targetKey);
610
820
  setValue(shadow?.text ?? "");
611
821
  setAnnotations(shadow?.annotations ?? []);
822
+ setPolicy(policyRef.current);
612
823
  setAnnotationReviewTargetId(null);
613
824
  setSending(false);
614
825
  setOptimisticSends(restoredOptimisticSends);
615
826
  setSteering(
616
827
  pendingOperationRef.current?.delivery === "steer"
617
828
  ? {
618
- phase: "submitting",
829
+ phase: "failed",
619
830
  text: pendingOperationRef.current.input.text,
620
831
  clientEventId: pendingOperationRef.current.input.clientEventId ?? null,
621
832
  triggerEventId: null,
622
833
  turnId: null,
834
+ error: "Not confirmed. Retry to check the same direction change.",
835
+ outcomeUnknown: true,
623
836
  }
624
837
  : null,
625
838
  );
626
839
  setPausing(false);
627
- setControlStopping(null);
840
+ setAcceptedControl(null);
628
841
  setResuming(false);
629
842
  setError(null);
843
+ setDraftReadError(null);
630
844
  setDraft(null);
631
845
  setDraftLoading(Boolean(sessionId) && durableDrafts);
632
846
  setDraftSaving(false);
633
847
  setDraftConflict(null);
634
848
  setRestoredResources(shadow?.resources ?? []);
635
- }, [durableDrafts, pendingOperationKey, sessionId, targetKey]);
849
+ }, [durableDrafts, options.initialPolicy, pendingOperationKey, sessionId, targetKey]);
850
+ useEffect(
851
+ () => () => {
852
+ if (draftReadRetryRef.current.timer !== null) {
853
+ clearTimeout(draftReadRetryRef.current.timer);
854
+ }
855
+ targetGeneration.current += 1;
856
+ draftReadGeneration.current += 1;
857
+ draftReadInFlightRef.current = null;
858
+ draftReadAbortRef.current?.abort();
859
+ draftReadAbortRef.current = null;
860
+ },
861
+ [],
862
+ );
636
863
 
637
864
  const setOptimisticDraftShadow = useCallback(
638
865
  (shadow: ComposerDraftShadow): void => {
@@ -643,6 +870,7 @@ export function useComposer(
643
870
  text: shadow.text,
644
871
  resources: [...shadow.resources],
645
872
  annotations: cloneAnnotations(shadow.annotations),
873
+ ...(shadow.policy ? { policy: { ...shadow.policy } } : {}),
646
874
  },
647
875
  }));
648
876
  optimisticSendsRef.current = next;
@@ -656,15 +884,18 @@ export function useComposer(
656
884
  (next: ComposerDraft): void => {
657
885
  if (targetKeyRef.current !== targetKey) return;
658
886
  if (!durableDrafts) {
887
+ const nextPolicy = policyFromDraft(next);
659
888
  localEditRevision.current += 1;
660
889
  valueRef.current = next.text;
661
890
  annotationsRef.current = next.annotations ?? [];
891
+ policyRef.current = nextPolicy;
662
892
  restoredResourcesRef.current = next.resources;
663
893
  draftRef.current = null;
664
894
  lastSavedSignature.current = null;
665
895
  setDraft(null);
666
896
  setValue(next.text);
667
897
  setAnnotations(next.annotations ?? []);
898
+ setPolicy(nextPolicy);
668
899
  setRestoredResources(next.resources);
669
900
  pendingOperationRef.current = updatePendingComposerShadow(
670
901
  pendingOperationKey,
@@ -676,6 +907,7 @@ export function useComposer(
676
907
  resolveSendExtras(sendExtrasRef.current).resources ?? [],
677
908
  ),
678
909
  annotations: next.annotations ?? [],
910
+ policy: nextPolicy,
679
911
  },
680
912
  );
681
913
  setOptimisticDraftShadow({
@@ -685,12 +917,15 @@ export function useComposer(
685
917
  resolveSendExtras(sendExtrasRef.current).resources ?? [],
686
918
  ),
687
919
  annotations: next.annotations ?? [],
920
+ policy: nextPolicy,
688
921
  });
689
922
  setDraftConflict(null);
690
923
  return;
691
924
  }
925
+ const nextPolicy = policyFromDraft(next);
692
926
  valueRef.current = next.text;
693
927
  annotationsRef.current = next.annotations ?? [];
928
+ policyRef.current = nextPolicy;
694
929
  draftRef.current = next;
695
930
  restoredResourcesRef.current = next.resources;
696
931
  lastSavedSignature.current = draftSignature(draftPayload(next));
@@ -698,6 +933,7 @@ export function useComposer(
698
933
  setDraft(next);
699
934
  setValue(next.text);
700
935
  setAnnotations(next.annotations ?? []);
936
+ setPolicy(nextPolicy);
701
937
  setRestoredResources(next.resources);
702
938
  pendingOperationRef.current = updatePendingComposerShadow(
703
939
  pendingOperationKey,
@@ -709,6 +945,7 @@ export function useComposer(
709
945
  resolveSendExtras(sendExtrasRef.current).resources ?? [],
710
946
  ),
711
947
  annotations: next.annotations ?? [],
948
+ policy: nextPolicy,
712
949
  },
713
950
  );
714
951
  setOptimisticDraftShadow({
@@ -718,9 +955,9 @@ export function useComposer(
718
955
  resolveSendExtras(sendExtrasRef.current).resources ?? [],
719
956
  ),
720
957
  annotations: next.annotations ?? [],
958
+ policy: nextPolicy,
721
959
  });
722
960
  setDraftConflict(null);
723
- onDraftAppliedRef.current?.(next);
724
961
  },
725
962
  [durableDrafts, pendingOperationKey, setOptimisticDraftShadow, targetKey],
726
963
  );
@@ -730,23 +967,54 @@ export function useComposer(
730
967
  if (targetKeyRef.current !== targetKey) return;
731
968
  if (!sessionId || !durableDrafts) {
732
969
  setDraftLoading(false);
970
+ setDraftReadError(null);
733
971
  return;
734
972
  }
973
+ let retry = draftReadRetryRef.current;
974
+ if (retry.targetKey !== targetKey) {
975
+ if (retry.timer !== null) clearTimeout(retry.timer);
976
+ retry = { targetKey, failures: 0, timer: null };
977
+ draftReadRetryRef.current = retry;
978
+ }
979
+ const scheduleRetry = (): void => {
980
+ if (retry.timer !== null) return;
981
+ retry.timer = setTimeout(
982
+ () => {
983
+ if (draftReadRetryRef.current !== retry || targetKeyRef.current !== targetKey) return;
984
+ retry.timer = null;
985
+ void loadDraft(false);
986
+ },
987
+ Math.min(25_000, 1_000 * 2 ** (retry.failures - 1)) * (0.8 + Math.random() * 0.4),
988
+ );
989
+ };
990
+ if (!replaceLocal && retry.timer !== null) return;
991
+ if (draftReadInFlightRef.current === targetKey) return;
992
+ if (retry.timer !== null) {
993
+ clearTimeout(retry.timer);
994
+ retry.timer = null;
995
+ }
996
+ draftReadInFlightRef.current = targetKey;
735
997
  const generation = targetGeneration.current;
736
998
  const readTicket = ++draftReadGeneration.current;
999
+ const requestAbort = new AbortController();
1000
+ draftReadAbortRef.current?.abort();
1001
+ draftReadAbortRef.current = requestAbort;
737
1002
  const localAtStart = localEditRevision.current;
738
1003
  const baseAtStart = draftRef.current;
739
- const extrasAtStart = resolveSendExtras(sendExtrasRef.current);
1004
+ const policyAtStart = policyRef.current;
740
1005
  const localSignatureAtStart = baseAtStart
741
- ? draftSignature(
742
- composerDraftPayload(
743
- baseAtStart,
744
- valueRef.current,
745
- restoredResourcesRef.current,
746
- annotationsRef.current,
747
- extrasAtStart,
748
- ),
749
- )
1006
+ ? policyAtStart
1007
+ ? draftSignature(
1008
+ composerDraftPayload(
1009
+ baseAtStart,
1010
+ valueRef.current,
1011
+ restoredResourcesRef.current,
1012
+ annotationsRef.current,
1013
+ policyAtStart,
1014
+ resolveSendExtras(sendExtrasRef.current).resources ?? [],
1015
+ ),
1016
+ )
1017
+ : null
750
1018
  : null;
751
1019
  const localWasDirtyAtStart =
752
1020
  localSignatureAtStart === null
@@ -760,7 +1028,9 @@ export function useComposer(
760
1028
  setDraftLoading(true);
761
1029
  }
762
1030
  try {
763
- const fetched = await client.getComposerDraft(workspaceId, sessionId);
1031
+ const fetched = await client.getComposerDraft(workspaceId, sessionId, {
1032
+ signal: requestAbort.signal,
1033
+ });
764
1034
  if (
765
1035
  generation !== targetGeneration.current ||
766
1036
  targetKeyRef.current !== targetKey ||
@@ -768,8 +1038,17 @@ export function useComposer(
768
1038
  ) {
769
1039
  return;
770
1040
  }
1041
+ if (retry.timer !== null) clearTimeout(retry.timer);
1042
+ retry.failures = 0;
1043
+ retry.timer = null;
1044
+ // Recovery also counts when the server returns the same draft revision.
1045
+ setDraftReadError(null);
771
1046
  const currentRevision = draftRef.current?.revision ?? -1;
772
- if (fetched.revision >= currentRevision) {
1047
+ // Reconnect and client-generation effects can legitimately ask for the
1048
+ // draft again. A same-revision response is not new authority: publishing
1049
+ // its freshly decoded object back into React creates a response -> render
1050
+ // -> read feedback loop. Hard reload remains the explicit exception.
1051
+ if (replaceLocal || fetched.revision > currentRevision) {
773
1052
  draftRef.current = fetched;
774
1053
  setDraft(fetched);
775
1054
  setDraftConflict(null);
@@ -782,42 +1061,64 @@ export function useComposer(
782
1061
  valueRef.current = shadow.text;
783
1062
  restoredResourcesRef.current = shadow.resources;
784
1063
  annotationsRef.current = shadow.annotations;
1064
+ if (shadow.policy) policyRef.current = shadow.policy;
785
1065
  localEditRevision.current ||= 1;
786
1066
  setValue(shadow.text);
787
1067
  setRestoredResources(shadow.resources);
788
1068
  setAnnotations(shadow.annotations);
1069
+ if (shadow.policy) setPolicy(shadow.policy);
789
1070
  } else if (
790
1071
  replaceLocal ||
791
1072
  (!localWasDirtyAtStart && localAtStart === localEditRevision.current)
792
1073
  ) {
793
- // Model/effort/latency ride in sendExtras (outside
794
- // localEditRevision). If
795
- // the picker changed during this fetch, skip onDraftApplied so a
796
- // stale server policy cannot undo the operator's pick.
797
- const extrasNow = resolveSendExtras(sendExtrasRef.current);
798
- const pickerChangedDuringFetch =
799
- extrasNow.model !== extrasAtStart.model ||
800
- extrasNow.reasoningEffort !== extrasAtStart.reasoningEffort ||
801
- extrasNow.latencyMode !== extrasAtStart.latencyMode;
1074
+ const fetchedPolicy = policyFromDraft(fetched);
802
1075
  valueRef.current = fetched.text;
803
1076
  restoredResourcesRef.current = fetched.resources;
804
1077
  annotationsRef.current = fetched.annotations ?? [];
1078
+ policyRef.current = fetchedPolicy;
805
1079
  lastSavedSignature.current = draftSignature(draftPayload(fetched));
806
1080
  setValue(fetched.text);
807
1081
  setRestoredResources(fetched.resources);
808
1082
  setAnnotations(fetched.annotations ?? []);
809
- if (replaceLocal || !pickerChangedDuringFetch) {
810
- onDraftAppliedRef.current?.(fetched);
1083
+ setPolicy(fetchedPolicy);
1084
+ } else {
1085
+ // Keep the in-flight edit, but the fetched row is still the OCC
1086
+ // base. First hydrate has no local policy until this read lands;
1087
+ // without it autosave cannot persist the typed text.
1088
+ lastSavedSignature.current = draftSignature(draftPayload(fetched));
1089
+ if (!policyRef.current) {
1090
+ const fetchedPolicy = policyFromDraft(fetched);
1091
+ policyRef.current = fetchedPolicy;
1092
+ setPolicy(fetchedPolicy);
811
1093
  }
812
1094
  }
813
1095
  }
814
1096
  } catch (cause) {
815
1097
  if (
1098
+ !requestAbort.signal.aborted &&
816
1099
  generation === targetGeneration.current &&
817
1100
  targetKeyRef.current === targetKey &&
818
1101
  readTicket === draftReadGeneration.current
819
1102
  ) {
820
- setError(asError(cause));
1103
+ const problem = asError(cause);
1104
+ const timedOut = problem.name === "TimeoutError";
1105
+ setDraftReadError(
1106
+ timedOut
1107
+ ? new ComposerStateError("Draft sync timed out. Retrying…", { cause })
1108
+ : problem,
1109
+ );
1110
+ if (
1111
+ timedOut ||
1112
+ cause instanceof TypeError ||
1113
+ (cause && typeof cause === "object" && "retryable" in cause && cause.retryable === true)
1114
+ ) {
1115
+ retry.failures += 1;
1116
+ scheduleRetry();
1117
+ } else {
1118
+ if (retry.timer !== null) clearTimeout(retry.timer);
1119
+ retry.failures = 0;
1120
+ retry.timer = null;
1121
+ }
821
1122
  }
822
1123
  } finally {
823
1124
  if (
@@ -827,6 +1128,12 @@ export function useComposer(
827
1128
  ) {
828
1129
  setDraftLoading(false);
829
1130
  }
1131
+ if (draftReadInFlightRef.current === targetKey) {
1132
+ draftReadInFlightRef.current = null;
1133
+ }
1134
+ if (draftReadAbortRef.current === requestAbort) {
1135
+ draftReadAbortRef.current = null;
1136
+ }
830
1137
  }
831
1138
  },
832
1139
  [client, durableDrafts, sessionId, targetKey, workspaceId],
@@ -930,32 +1237,41 @@ export function useComposer(
930
1237
  }, [options.events, steering]);
931
1238
 
932
1239
  useEffect(() => {
933
- if (!controlStopping) return;
1240
+ if (!acceptedControl) return;
934
1241
  const effectiveControl = options.effectiveControl;
935
- if (!effectiveControl || effectiveControl.controlVersion < controlStopping.controlVersion) {
936
- return;
937
- }
938
- if (
939
- effectiveControl.controlVersion === controlStopping.controlVersion &&
940
- effectiveControl.controlEtag === controlStopping.controlEtag &&
941
- effectiveControl.settlement !== null
942
- ) {
1242
+ if (!effectiveControl || effectiveControl.controlVersion < acceptedControl.controlVersion) {
943
1243
  return;
944
1244
  }
945
- // A newer control revision, an impossible same-version/different-etag
946
- // projection, or quiescence at this exact revision supersedes the local
947
- // mutation receipt. SessionChrome then falls back to durable queue truth.
948
- setControlStopping((current) => (current === controlStopping ? null : current));
949
- }, [controlStopping, options.effectiveControl]);
1245
+ setAcceptedControl((current) => (current === acceptedControl ? null : current));
1246
+ }, [acceptedControl, options.effectiveControl]);
950
1247
 
951
1248
  const currentDraftPayload = useCallback((): SaveComposerDraftRequest | null => {
952
1249
  if (!durableDrafts || targetKeyRef.current !== targetKey) return null;
953
1250
  const base = draftRef.current;
954
- if (!base) return null;
955
- const extras = resolveSendExtras(sendExtrasRef.current);
956
- return composerDraftPayload(base, value, restoredResources, annotations, extras);
1251
+ const currentPolicy = policyRef.current;
1252
+ if (!base || !currentPolicy) return null;
1253
+ return composerDraftPayload(
1254
+ base,
1255
+ value,
1256
+ restoredResources,
1257
+ annotations,
1258
+ currentPolicy,
1259
+ resolveSendExtras(sendExtrasRef.current).resources ?? [],
1260
+ );
957
1261
  }, [annotations, durableDrafts, restoredResources, targetKey, value]);
958
1262
 
1263
+ const adoptDraftBase = useCallback(
1264
+ (next: ComposerDraft): void => {
1265
+ if (targetKeyRef.current !== targetKey) return;
1266
+ draftRef.current = next;
1267
+ setDraft(next);
1268
+ lastSavedSignature.current = draftSignature(draftPayload(next));
1269
+ setDraftConflict(null);
1270
+ setError(null);
1271
+ },
1272
+ [targetKey],
1273
+ );
1274
+
959
1275
  const persistPayload = useCallback(
960
1276
  async (payload: SaveComposerDraftRequest): Promise<boolean> => {
961
1277
  const ownedTargetKey = targetKey;
@@ -981,30 +1297,14 @@ export function useComposer(
981
1297
  }
982
1298
  setDraftSaving(true);
983
1299
  try {
984
- const saved = await saveComposerDraftWithStaleRetry({
985
- client,
986
- workspaceId,
987
- sessionId,
988
- request,
989
- onAdoptRemote: (remote) => {
990
- draftRef.current = remote;
991
- setDraft(remote);
992
- },
993
- });
1300
+ const saved = await client.saveComposerDraft(workspaceId, sessionId, request);
994
1301
  if (
995
1302
  targetKeyRef.current !== ownedTargetKey ||
996
1303
  targetGeneration.current !== ownedGeneration
997
1304
  ) {
998
1305
  return;
999
1306
  }
1000
- draftRef.current = saved;
1001
- setDraft(saved);
1002
- lastSavedSignature.current = draftSignature({
1003
- ...request,
1004
- expectedRevision: saved.revision,
1005
- });
1006
- setDraftConflict(null);
1007
- setError(null);
1307
+ adoptDraftBase(saved);
1008
1308
  success = true;
1009
1309
  } catch (cause) {
1010
1310
  if (
@@ -1028,7 +1328,7 @@ export function useComposer(
1028
1328
  await saveChain.current;
1029
1329
  return success;
1030
1330
  },
1031
- [client, durableDrafts, sessionId, targetKey, workspaceId],
1331
+ [adoptDraftBase, client, durableDrafts, sessionId, targetKey, workspaceId],
1032
1332
  );
1033
1333
 
1034
1334
  const replaceOptimisticSends = useCallback(
@@ -1058,17 +1358,46 @@ export function useComposer(
1058
1358
  );
1059
1359
 
1060
1360
  useEffect(() => {
1061
- const acceptedIds = new Set(
1062
- (options.events ?? [])
1063
- .filter((event) => event.type === "user.message" && event.clientEventId)
1064
- .map((event) => event.clientEventId as string),
1361
+ const events = options.events ?? [];
1362
+ if (events.length === 0 || optimisticSendsRef.current.length === 0) return;
1363
+ const reconciled = optimisticSendsRef.current.map((operation) =>
1364
+ reconcileOptimisticSendFromEvents(operation, events),
1365
+ );
1366
+ const settledIds = new Set(
1367
+ reconciled.filter((item) => item.settled).map((item) => item.operation.clientEventId),
1065
1368
  );
1066
- if (acceptedIds.size === 0) return;
1067
- const accepted = optimisticSendsRef.current.filter((operation) =>
1068
- acceptedIds.has(operation.clientEventId),
1369
+ const changed = reconciled.some(
1370
+ (item, index) =>
1371
+ item.operation !== optimisticSendsRef.current[index] &&
1372
+ !settledIds.has(item.operation.clientEventId),
1069
1373
  );
1070
- for (const operation of accepted) markOptimisticAccepted(operation);
1071
- }, [markOptimisticAccepted, options.events]);
1374
+ for (const item of reconciled) {
1375
+ if (item.admitted && !optimisticCallbackIdsRef.current.has(item.operation.clientEventId)) {
1376
+ optimisticCallbackIdsRef.current.add(item.operation.clientEventId);
1377
+ onSent?.(item.operation.input.text, item.operation.input);
1378
+ }
1379
+ if (item.settled) markOptimisticAccepted(item.operation);
1380
+ }
1381
+ if (changed) {
1382
+ replaceOptimisticSends((current) =>
1383
+ current
1384
+ .map(
1385
+ (operation) =>
1386
+ reconciled.find((item) => item.operation.clientEventId === operation.clientEventId)
1387
+ ?.operation ?? operation,
1388
+ )
1389
+ .filter((operation) => !settledIds.has(operation.clientEventId)),
1390
+ );
1391
+ }
1392
+ if (durableDrafts && reconciled.some((item) => item.admitted)) void loadDraft(false);
1393
+ }, [
1394
+ durableDrafts,
1395
+ loadDraft,
1396
+ markOptimisticAccepted,
1397
+ onSent,
1398
+ options.events,
1399
+ replaceOptimisticSends,
1400
+ ]);
1072
1401
 
1073
1402
  const processOptimisticSends = useCallback((): void => {
1074
1403
  if (!sessionId || optimisticProcessorBusyRef.current) return;
@@ -1082,32 +1411,92 @@ export function useComposer(
1082
1411
  const ownedGeneration = targetGeneration.current;
1083
1412
  optimisticProcessorBusyRef.current = true;
1084
1413
  void (async () => {
1414
+ let mutationFailureObserved = false;
1415
+ const markFailed = (cause: unknown, outcomeUnknown: boolean): void => {
1416
+ if (
1417
+ targetKeyRef.current !== ownedTargetKey ||
1418
+ targetGeneration.current !== ownedGeneration
1419
+ ) {
1420
+ return;
1421
+ }
1422
+ const problem = asError(cause);
1423
+ replaceOptimisticSends((current) =>
1424
+ current.map((candidate) =>
1425
+ candidate.clientEventId === operation.clientEventId
1426
+ ? {
1427
+ ...candidate,
1428
+ state: "failed",
1429
+ error: formatError(
1430
+ cause,
1431
+ outcomeUnknown &&
1432
+ !isOutcomeUnknownError(problem) &&
1433
+ !(problem instanceof ComposerReconciliationRequiredError)
1434
+ ? `The request could not be confirmed. Check its status before retrying.${problem instanceof OpenGeniApiError && problem.correlationId ? ` Reference: ${problem.correlationId}.` : ""}`
1435
+ : composerSubmissionErrorMessage(problem),
1436
+ ),
1437
+ retryable: outcomeUnknown || composerSubmissionCanRetry(problem),
1438
+ outcomeUnknown,
1439
+ correlationId:
1440
+ problem instanceof OpenGeniApiError
1441
+ ? problem.correlationId
1442
+ : candidate.correlationId,
1443
+ }
1444
+ : candidate,
1445
+ ),
1446
+ );
1447
+ };
1085
1448
  try {
1086
1449
  if (operation.outcomeUnknown) {
1087
- const events = await client.listEvents(workspaceId, sessionId, {
1088
- includeTypes: ["user.message"],
1089
- limit: 100,
1090
- payloadMode: "none",
1091
- });
1092
- if (
1093
- events.some(
1094
- (event) =>
1095
- event.type === "user.message" && event.clientEventId === operation.clientEventId,
1096
- )
1097
- ) {
1098
- markOptimisticAccepted(operation);
1450
+ let events: SessionEvent[];
1451
+ try {
1452
+ events = await client.listEvents(workspaceId, sessionId, {
1453
+ includeTypes: [
1454
+ "user.message",
1455
+ "turn.queued",
1456
+ "turn.started",
1457
+ "turn.completed",
1458
+ "turn.failed",
1459
+ "turn.cancelled",
1460
+ "turn.superseded",
1461
+ ],
1462
+ limit: 250,
1463
+ payloadMode: "full",
1464
+ });
1465
+ } catch (cause) {
1466
+ // This read cannot settle the prior mutation in either direction.
1467
+ // Preserve its exact key and uncertain status until reconciliation
1468
+ // succeeds or that same mutation is actually replayed.
1469
+ markFailed(cause, true);
1470
+ return;
1471
+ }
1472
+ const reconciled = reconcileOptimisticSendFromEvents(operation, events);
1473
+ if (reconciled.admitted) {
1474
+ if (durableDrafts) await loadDraft(false);
1475
+ if (reconciled.settled) {
1476
+ markOptimisticAccepted(reconciled.operation);
1477
+ } else {
1478
+ replaceOptimisticSends((current) =>
1479
+ current.map((candidate) =>
1480
+ candidate.clientEventId === operation.clientEventId
1481
+ ? reconciled.operation
1482
+ : candidate,
1483
+ ),
1484
+ );
1485
+ if (!optimisticCallbackIdsRef.current.has(operation.clientEventId)) {
1486
+ optimisticCallbackIdsRef.current.add(operation.clientEventId);
1487
+ onSent?.(reconciled.operation.input.text, reconciled.operation.input);
1488
+ }
1489
+ }
1099
1490
  return;
1100
1491
  }
1101
1492
  if (!operation.canRetry) {
1102
- throw new Error(
1103
- "OpenGeni cannot safely retry this uncertain request after remount; reconcile the session before sending again.",
1104
- );
1493
+ throw new ComposerReconciliationRequiredError();
1105
1494
  }
1106
1495
  }
1107
1496
  let expectedDraftRevision: number | undefined;
1108
1497
  if (durableDrafts && operation.draftPayload) {
1109
1498
  if (!(await persistPayload(operation.draftPayload))) {
1110
- throw new Error("The message draft could not be saved before delivery.");
1499
+ throw new ComposerStateError("The message draft could not be saved before delivery.");
1111
1500
  }
1112
1501
  expectedDraftRevision = draftRef.current?.revision;
1113
1502
  }
@@ -1121,20 +1510,83 @@ export function useComposer(
1121
1510
  ...operation.input,
1122
1511
  ...(expectedDraftRevision !== undefined ? { expectedDraftRevision } : {}),
1123
1512
  };
1124
- await client.sendMessage(workspaceId, sessionId, wireInput);
1513
+ let acceptedResult: SubmitComposerDraftResponse | null = null;
1514
+ let acceptedEvent: SessionEvent | null = null;
1515
+ if (durableDrafts) {
1516
+ if (!operation.draftPayload || expectedDraftRevision === undefined) {
1517
+ throw new ComposerStateError("The durable composer draft is not ready for delivery.");
1518
+ }
1519
+ const result = await client
1520
+ .submitComposerDraft(workspaceId, sessionId, {
1521
+ text: operation.draftPayload.text,
1522
+ annotations: operation.draftPayload.annotations,
1523
+ resources: operation.draftPayload.resources,
1524
+ model: operation.draftPayload.model,
1525
+ reasoningEffort: operation.draftPayload.reasoningEffort,
1526
+ latencyMode: operation.draftPayload.latencyMode,
1527
+ expectedDraftRevision,
1528
+ clientEventId: operation.clientEventId,
1529
+ delivery: "send",
1530
+ ...(wireInput.controlEtag ? { controlEtag: wireInput.controlEtag } : {}),
1531
+ ...(wireInput.modelContext ? { modelContext: wireInput.modelContext } : {}),
1532
+ ...(wireInput.mcpCredentialUpdates
1533
+ ? { mcpCredentialUpdates: wireInput.mcpCredentialUpdates }
1534
+ : {}),
1535
+ ...(wireInput.connectionAccounts
1536
+ ? { connectionAccounts: wireInput.connectionAccounts }
1537
+ : {}),
1538
+
1539
+ ...(wireInput.personalResourceAttachment
1540
+ ? { personalResourceAttachment: wireInput.personalResourceAttachment }
1541
+ : {}),
1542
+ })
1543
+ .catch((cause: unknown) => {
1544
+ mutationFailureObserved = true;
1545
+ throw cause;
1546
+ });
1547
+ acceptedResult = result;
1548
+ adoptDraftBase(result.draft);
1549
+ } else {
1550
+ acceptedEvent = await client
1551
+ .sendMessage(workspaceId, sessionId, wireInput)
1552
+ .catch((cause: unknown) => {
1553
+ mutationFailureObserved = true;
1554
+ throw cause;
1555
+ });
1556
+ }
1125
1557
  if (
1126
1558
  targetKeyRef.current !== ownedTargetKey ||
1127
1559
  targetGeneration.current !== ownedGeneration
1128
1560
  ) {
1129
1561
  return;
1130
1562
  }
1131
- if (options.events === undefined) {
1563
+ const acceptedRouting =
1564
+ acceptedResult?.routing ?? promptRoutingFromAcceptedEvent(acceptedEvent);
1565
+ const acceptedDestination =
1566
+ acceptedRouting === "queued_for_execution"
1567
+ ? "queue"
1568
+ : acceptedRouting === "accepted_for_execution" ||
1569
+ acceptedRouting === "accepted_for_steering"
1570
+ ? "chat"
1571
+ : operation.destination;
1572
+ if (options.events === undefined && acceptedDestination === "chat") {
1132
1573
  markOptimisticAccepted({ ...operation, input: wireInput });
1133
1574
  } else {
1134
1575
  replaceOptimisticSends((current) =>
1135
1576
  current.map((candidate) =>
1136
1577
  candidate.clientEventId === operation.clientEventId
1137
- ? { ...candidate, input: wireInput, state: "queued", error: undefined }
1578
+ ? {
1579
+ ...candidate,
1580
+ input: wireInput,
1581
+ destination: acceptedDestination,
1582
+ turnId: acceptedResult?.turn.id ?? candidate.turnId,
1583
+ triggerEventId:
1584
+ acceptedResult?.accepted.id ?? acceptedEvent?.id ?? candidate.triggerEventId,
1585
+ appliedQueueVersion:
1586
+ acceptedResult?.receipt?.appliedQueueVersion ?? candidate.appliedQueueVersion,
1587
+ state: "queued",
1588
+ error: undefined,
1589
+ }
1138
1590
  : candidate,
1139
1591
  ),
1140
1592
  );
@@ -1143,41 +1595,24 @@ export function useComposer(
1143
1595
  optimisticCallbackIdsRef.current.add(operation.clientEventId);
1144
1596
  onSent?.(wireInput.text, wireInput);
1145
1597
  }
1146
- if (durableDrafts && draftRef.current) {
1147
- const cleared = {
1148
- ...draftRef.current,
1149
- revision: 0,
1150
- text: "",
1151
- resources: [],
1152
- annotations: [],
1153
- sourceTurnId: null,
1154
- sourceTurnVersion: null,
1155
- updatedAt: null,
1156
- };
1157
- draftRef.current = cleared;
1158
- setDraft(cleared);
1159
- lastSavedSignature.current = draftSignature(draftPayload(cleared));
1160
- }
1161
1598
  } catch (cause) {
1162
- const problem = asError(cause);
1163
- const outcomeUnknown = isOutcomeUnknownError(cause) || operation.outcomeUnknown === true;
1164
- if (
1165
- targetKeyRef.current === ownedTargetKey &&
1166
- targetGeneration.current === ownedGeneration
1167
- ) {
1168
- replaceOptimisticSends((current) =>
1169
- current.map((candidate) =>
1170
- candidate.clientEventId === operation.clientEventId
1171
- ? {
1172
- ...candidate,
1173
- state: "failed",
1174
- error: problem.message,
1175
- outcomeUnknown,
1176
- }
1177
- : candidate,
1178
- ),
1179
- );
1599
+ const current = optimisticSendsRef.current.find(
1600
+ (candidate) => candidate.clientEventId === operation.clientEventId,
1601
+ );
1602
+ // A live durable event can prove admission before the original HTTP
1603
+ // response times out. Never regress that confirmed state to failure.
1604
+ if (!current || (current.state === "queued" && current.outcomeUnknown === false)) {
1605
+ return;
1180
1606
  }
1607
+ const problem = asError(cause);
1608
+ onDeliveryErrorRef.current?.(problem, operation.input, "send");
1609
+ // Only an actual mutation replay can replace prior uncertainty with a
1610
+ // definitive response. Local preparation failures remain retry-only.
1611
+ const outcomeUnknown =
1612
+ operation.outcomeUnknown && !mutationFailureObserved
1613
+ ? true
1614
+ : isOutcomeUnknownError(cause);
1615
+ markFailed(cause, outcomeUnknown);
1181
1616
  } finally {
1182
1617
  optimisticProcessorBusyRef.current = false;
1183
1618
  if (targetKeyRef.current === ownedTargetKey) {
@@ -1186,9 +1621,12 @@ export function useComposer(
1186
1621
  }
1187
1622
  })();
1188
1623
  }, [
1624
+ adoptDraftBase,
1189
1625
  client,
1190
1626
  durableDrafts,
1627
+ formatError,
1191
1628
  markOptimisticAccepted,
1629
+ loadDraft,
1192
1630
  onSent,
1193
1631
  options.events,
1194
1632
  persistPayload,
@@ -1214,11 +1652,13 @@ export function useComposer(
1214
1652
  resolveSendExtras(sendExtrasRef.current).resources ?? [],
1215
1653
  ),
1216
1654
  annotations: annotationsRef.current,
1655
+ ...(policyRef.current ? { policy: policyRef.current } : {}),
1217
1656
  };
1218
1657
  if (
1219
1658
  pending.newerShadow.text !== shadow.text ||
1220
1659
  JSON.stringify(pending.newerShadow.resources) !== JSON.stringify(shadow.resources) ||
1221
- JSON.stringify(pending.newerShadow.annotations) !== JSON.stringify(shadow.annotations)
1660
+ JSON.stringify(pending.newerShadow.annotations) !== JSON.stringify(shadow.annotations) ||
1661
+ JSON.stringify(pending.newerShadow.policy) !== JSON.stringify(shadow.policy)
1222
1662
  ) {
1223
1663
  pendingOperationRef.current = updatePendingComposerShadow(
1224
1664
  pendingOperationKey,
@@ -1237,11 +1677,13 @@ export function useComposer(
1237
1677
  resolveSendExtras(sendExtrasRef.current).resources ?? [],
1238
1678
  ),
1239
1679
  annotations: annotationsRef.current,
1680
+ ...(policyRef.current ? { policy: policyRef.current } : {}),
1240
1681
  };
1241
1682
  if (
1242
1683
  optimistic.newerShadow.text !== shadow.text ||
1243
1684
  JSON.stringify(optimistic.newerShadow.resources) !== JSON.stringify(shadow.resources) ||
1244
- JSON.stringify(optimistic.newerShadow.annotations) !== JSON.stringify(shadow.annotations)
1685
+ JSON.stringify(optimistic.newerShadow.annotations) !== JSON.stringify(shadow.annotations) ||
1686
+ JSON.stringify(optimistic.newerShadow.policy) !== JSON.stringify(shadow.policy)
1245
1687
  ) {
1246
1688
  setOptimisticDraftShadow(shadow);
1247
1689
  }
@@ -1290,6 +1732,7 @@ export function useComposer(
1290
1732
  const rawText = explicit ?? draftAtSend;
1291
1733
  const hasText = rawText.trim().length > 0;
1292
1734
  const hasAnnotations = annotationsAtSend.length > 0;
1735
+ const currentPolicy = policyRef.current;
1293
1736
  const annotationsComplete = annotationsAtSend.every(
1294
1737
  (annotation) => annotation.note.trim().length > 0,
1295
1738
  );
@@ -1300,6 +1743,7 @@ export function useComposer(
1300
1743
  if (
1301
1744
  (!pending && (!annotationsComplete || (!hasText && !hasResources && !hasAnnotations))) ||
1302
1745
  !sessionId ||
1746
+ !currentPolicy ||
1303
1747
  sending ||
1304
1748
  sendBlockedRef.current?.() === true ||
1305
1749
  targetKeyRef.current !== ownedTargetKey
@@ -1323,22 +1767,6 @@ export function useComposer(
1323
1767
  JSON.stringify(operation.resourcesAtSend);
1324
1768
  const annotationsWereUnchanged =
1325
1769
  JSON.stringify(annotationsRef.current) === JSON.stringify(operation.annotationsAtSend);
1326
- const previousDraft = draftRef.current;
1327
- if (previousDraft) {
1328
- const cleared = {
1329
- ...previousDraft,
1330
- revision: 0,
1331
- text: "",
1332
- resources: [],
1333
- annotations: [],
1334
- sourceTurnId: null,
1335
- sourceTurnVersion: null,
1336
- updatedAt: null,
1337
- };
1338
- draftRef.current = cleared;
1339
- setDraft(cleared);
1340
- lastSavedSignature.current = draftSignature(draftPayload(cleared));
1341
- }
1342
1770
  if (resourcesWereUnchanged) {
1343
1771
  restoredResourcesRef.current = [];
1344
1772
  setRestoredResources([]);
@@ -1356,6 +1784,40 @@ export function useComposer(
1356
1784
  };
1357
1785
 
1358
1786
  const deliver = async (operation: PendingComposerOperation) => {
1787
+ if (durableDrafts) {
1788
+ const input = operation.input;
1789
+ if (
1790
+ input.expectedDraftRevision === undefined ||
1791
+ input.clientEventId === undefined ||
1792
+ input.model === undefined ||
1793
+ input.reasoningEffort === undefined ||
1794
+ input.latencyMode === undefined
1795
+ ) {
1796
+ throw new ComposerStateError("The durable composer draft is not ready for delivery.");
1797
+ }
1798
+ const result = await client.submitComposerDraft(workspaceId, sessionId, {
1799
+ text: input.text,
1800
+ annotations: input.annotations ?? [],
1801
+ resources: input.resources ?? [],
1802
+ model: input.model,
1803
+ reasoningEffort: input.reasoningEffort,
1804
+ latencyMode: input.latencyMode,
1805
+ expectedDraftRevision: input.expectedDraftRevision,
1806
+ clientEventId: input.clientEventId,
1807
+ delivery: operation.delivery,
1808
+ ...(input.controlEtag ? { controlEtag: input.controlEtag } : {}),
1809
+ ...(input.modelContext ? { modelContext: input.modelContext } : {}),
1810
+ ...(input.mcpCredentialUpdates
1811
+ ? { mcpCredentialUpdates: input.mcpCredentialUpdates }
1812
+ : {}),
1813
+ ...(input.connectionAccounts ? { connectionAccounts: input.connectionAccounts } : {}),
1814
+ ...(input.personalResourceAttachment
1815
+ ? { personalResourceAttachment: input.personalResourceAttachment }
1816
+ : {}),
1817
+ });
1818
+ adoptDraftBase(result.draft);
1819
+ return result;
1820
+ }
1359
1821
  if (operation.delivery === "steer") {
1360
1822
  return await client.steerMessage(workspaceId, sessionId, operation.input);
1361
1823
  }
@@ -1364,6 +1826,9 @@ export function useComposer(
1364
1826
  };
1365
1827
 
1366
1828
  if (delivery === "steer") {
1829
+ if (steeringRef.current?.clientEventId !== pending?.input.clientEventId) {
1830
+ steeringOccurredAtRef.current = new Date().toISOString();
1831
+ }
1367
1832
  setSteering({
1368
1833
  phase: "submitting",
1369
1834
  text: rawText,
@@ -1394,7 +1859,20 @@ export function useComposer(
1394
1859
  targetKeyRef.current === ownedTargetKey &&
1395
1860
  targetGeneration.current === ownedGeneration
1396
1861
  ) {
1397
- setError(asError(cause));
1862
+ const problem = asError(cause);
1863
+ setError(problem);
1864
+ if (pending.delivery === "steer") {
1865
+ keepSteering = true;
1866
+ setSteering({
1867
+ phase: "failed",
1868
+ text: pending.input.text,
1869
+ clientEventId: pending.input.clientEventId ?? null,
1870
+ triggerEventId: null,
1871
+ turnId: null,
1872
+ error: "Not confirmed. Retry.",
1873
+ outcomeUnknown: true,
1874
+ });
1875
+ }
1398
1876
  }
1399
1877
  return false;
1400
1878
  }
@@ -1405,6 +1883,7 @@ export function useComposer(
1405
1883
  return false;
1406
1884
  }
1407
1885
  if (acceptedEvent) {
1886
+ if (durableDrafts) await loadDraft(false);
1408
1887
  if (pending.delivery === "steer") {
1409
1888
  keepSteering = true;
1410
1889
  setSteering({
@@ -1419,11 +1898,7 @@ export function useComposer(
1419
1898
  return true;
1420
1899
  }
1421
1900
  if (!pending.canRetry) {
1422
- setError(
1423
- new Error(
1424
- "OpenGeni cannot safely retry this uncertain request after remount; reconcile the session before sending again.",
1425
- ),
1426
- );
1901
+ setError(new ComposerReconciliationRequiredError());
1427
1902
  return false;
1428
1903
  }
1429
1904
  try {
@@ -1441,12 +1916,29 @@ export function useComposer(
1441
1916
  });
1442
1917
  }
1443
1918
  } catch (cause) {
1444
- if (pending.delivery === "steer") keepSteering = true;
1919
+ const problem = asError(cause);
1920
+ const outcomeUnknown = isOutcomeUnknownError(cause);
1921
+ onDeliveryErrorRef.current?.(problem, pending.input, pending.delivery);
1922
+ if (!outcomeUnknown) {
1923
+ clearPending();
1924
+ keepSteering = false;
1925
+ } else if (pending.delivery === "steer") {
1926
+ keepSteering = true;
1927
+ setSteering({
1928
+ phase: "failed",
1929
+ text: pending.input.text,
1930
+ clientEventId: pending.input.clientEventId ?? null,
1931
+ triggerEventId: null,
1932
+ turnId: null,
1933
+ error: "Not confirmed. Retry.",
1934
+ outcomeUnknown: true,
1935
+ });
1936
+ }
1445
1937
  if (
1446
1938
  targetKeyRef.current === ownedTargetKey &&
1447
1939
  targetGeneration.current === ownedGeneration
1448
1940
  ) {
1449
- setError(asError(cause));
1941
+ setError(problem);
1450
1942
  }
1451
1943
  return false;
1452
1944
  }
@@ -1478,10 +1970,10 @@ export function useComposer(
1478
1970
  // worker rejects whitespace-only text; a file-only message therefore
1479
1971
  // carries a minimal default so the attachments still get delivered.
1480
1972
  pendingClientEventId.current ??= generateClientEventId();
1973
+ const effectiveControl = effectiveControlRef.current;
1481
1974
  const input = composeSendInput(sendText, pendingClientEventId.current, extras, {
1482
- ...(options.effectiveControl?.controlEtag
1483
- ? { controlEtag: options.effectiveControl.controlEtag }
1484
- : {}),
1975
+ ...currentPolicy,
1976
+ ...(effectiveControl?.controlEtag ? { controlEtag: effectiveControl.controlEtag } : {}),
1485
1977
  ...(durableDrafts && draftRef.current
1486
1978
  ? { expectedDraftRevision: draftRef.current.revision }
1487
1979
  : {}),
@@ -1498,6 +1990,7 @@ export function useComposer(
1498
1990
  text: draftAtSend,
1499
1991
  resources: mergeResources(restoredResources, extras.resources ?? []),
1500
1992
  annotations: cloneAnnotations(annotationsAtSend),
1993
+ policy: currentPolicy,
1501
1994
  },
1502
1995
  clearDraftOnAccept: explicit === undefined,
1503
1996
  canRetry: true,
@@ -1528,10 +2021,20 @@ export function useComposer(
1528
2021
  });
1529
2022
  }
1530
2023
  } catch (cause) {
2024
+ onDeliveryErrorRef.current?.(asError(cause), operation.input, operation.delivery);
1531
2025
  if (!isOutcomeUnknownError(cause)) {
1532
2026
  clearPending();
1533
2027
  } else if (delivery === "steer") {
1534
2028
  keepSteering = true;
2029
+ setSteering({
2030
+ phase: "failed",
2031
+ text: operation.input.text,
2032
+ clientEventId: operation.input.clientEventId ?? null,
2033
+ triggerEventId: null,
2034
+ turnId: null,
2035
+ error: "Not confirmed. Retry.",
2036
+ outcomeUnknown: true,
2037
+ });
1535
2038
  }
1536
2039
  if (
1537
2040
  targetKeyRef.current === ownedTargetKey &&
@@ -1564,15 +2067,16 @@ export function useComposer(
1564
2067
  currentDraftPayload,
1565
2068
  durableDrafts,
1566
2069
  onSent,
1567
- options.effectiveControl?.controlEtag,
1568
2070
  persistPayload,
1569
2071
  restoredResources,
1570
2072
  annotations,
2073
+ adoptDraftBase,
1571
2074
  sending,
1572
2075
  sessionId,
1573
2076
  targetKey,
1574
2077
  value,
1575
2078
  workspaceId,
2079
+ loadDraft,
1576
2080
  ],
1577
2081
  );
1578
2082
 
@@ -1583,6 +2087,7 @@ export function useComposer(
1583
2087
  }
1584
2088
  const rawText = explicit ?? valueRef.current;
1585
2089
  const annotationsAtSend = cloneAnnotations(annotationsRef.current);
2090
+ const currentPolicy = policyRef.current;
1586
2091
  const hasText = rawText.trim().length > 0;
1587
2092
  const hasAnnotations = annotationsAtSend.length > 0;
1588
2093
  const annotationsComplete = annotationsAtSend.every(
@@ -1592,6 +2097,7 @@ export function useComposer(
1592
2097
  const resources = mergeResources(restoredResourcesRef.current, extras.resources ?? []);
1593
2098
  if (
1594
2099
  !sessionId ||
2100
+ !currentPolicy ||
1595
2101
  sending ||
1596
2102
  !annotationsComplete ||
1597
2103
  (!hasText && !hasAnnotations && resources.length === 0) ||
@@ -1602,16 +2108,24 @@ export function useComposer(
1602
2108
  }
1603
2109
  const sendText = hasText ? rawText : hasAnnotations ? "" : FILE_ONLY_MESSAGE_TEXT;
1604
2110
  const clientEventId = generateClientEventId();
2111
+ const effectiveControl = effectiveControlRef.current;
1605
2112
  const input = composeSendInput(sendText, clientEventId, extras, {
1606
- ...(options.effectiveControl?.controlEtag
1607
- ? { controlEtag: options.effectiveControl.controlEtag }
1608
- : {}),
2113
+ ...currentPolicy,
2114
+ ...(effectiveControl?.controlEtag ? { controlEtag: effectiveControl.controlEtag } : {}),
1609
2115
  resources,
1610
2116
  annotations: annotationsAtSend,
1611
2117
  });
1612
2118
  const currentPayload = currentDraftPayload();
2119
+ const hasEarlierUnsettledSend = optimisticSendsRef.current.some(
2120
+ (candidate) => candidate.state !== "failed" || candidate.outcomeUnknown === true,
2121
+ );
1613
2122
  const operation: OptimisticSendOperation = {
1614
2123
  clientEventId,
2124
+ delivery: "send",
2125
+ destination:
2126
+ effectiveControl?.state === "paused" || hasEarlierUnsettledSend
2127
+ ? "queue"
2128
+ : (sendDestinationRef.current?.() ?? "chat"),
1615
2129
  text: sendText,
1616
2130
  annotations: annotationsAtSend,
1617
2131
  resources,
@@ -1623,13 +2137,16 @@ export function useComposer(
1623
2137
  text: explicit === undefined ? "" : valueRef.current,
1624
2138
  resources: explicit === undefined ? [] : [...restoredResourcesRef.current],
1625
2139
  annotations: explicit === undefined ? [] : cloneAnnotations(annotationsRef.current),
2140
+ policy: currentPolicy,
1626
2141
  },
1627
2142
  canRetry: true,
1628
2143
  };
1629
- replaceOptimisticSends((current) => [...current, operation]);
1630
- queueMicrotask(processOptimisticSends);
1631
- setError(null);
1632
- onSubmitted?.(sendText, input);
2144
+ // The local submission acknowledgement owns the composer before any
2145
+ // host callback or queued processor can observe it. Hosts commonly use
2146
+ // onSubmitted to remove attachment/repository state, and those updates
2147
+ // may synchronously re-render the controlled composer. Clear the refs
2148
+ // first so that handoff cannot re-project the submitted text as the next
2149
+ // draft. The immutable operation above still owns the exact retry input.
1633
2150
  if (explicit === undefined) {
1634
2151
  valueRef.current = "";
1635
2152
  annotationsRef.current = [];
@@ -1640,13 +2157,16 @@ export function useComposer(
1640
2157
  setAnnotationReviewTargetId(null);
1641
2158
  setRestoredResources([]);
1642
2159
  }
2160
+ replaceOptimisticSends((current) => [...current, operation]);
2161
+ queueMicrotask(processOptimisticSends);
2162
+ setError(null);
2163
+ onSubmitted?.(sendText, input);
1643
2164
  return true;
1644
2165
  },
1645
2166
  [
1646
2167
  currentDraftPayload,
1647
2168
  dispatch,
1648
2169
  onSubmitted,
1649
- options.effectiveControl?.controlEtag,
1650
2170
  processOptimisticSends,
1651
2171
  replaceOptimisticSends,
1652
2172
  sending,
@@ -1658,6 +2178,12 @@ export function useComposer(
1658
2178
 
1659
2179
  const retryOptimisticMessage = useCallback(
1660
2180
  (clientEventId: string): void => {
2181
+ if (sendBlockedRef.current?.() === true) return;
2182
+ const currentSteering = steeringRef.current;
2183
+ if (currentSteering?.phase === "failed" && currentSteering.clientEventId === clientEventId) {
2184
+ void dispatch("steer");
2185
+ return;
2186
+ }
1661
2187
  replaceOptimisticSends((current) =>
1662
2188
  current.map((operation) => {
1663
2189
  if (operation.clientEventId !== clientEventId || operation.state !== "failed") {
@@ -1666,31 +2192,37 @@ export function useComposer(
1666
2192
  if (operation.outcomeUnknown) {
1667
2193
  return { ...operation, state: "sending", error: undefined };
1668
2194
  }
2195
+ if (operation.retryable === false) return operation;
1669
2196
  const nextClientEventId = generateClientEventId();
1670
- const retryExtras = resolveSendExtras(sendExtrasRef.current);
1671
- const retryInput = composeSendInput(operation.text, nextClientEventId, retryExtras, {
1672
- ...(options.effectiveControl?.controlEtag
1673
- ? { controlEtag: options.effectiveControl.controlEtag }
1674
- : {}),
1675
- resources: operation.resources,
1676
- annotations: operation.annotations,
1677
- });
2197
+ const currentPersonalResourceAttachment = resolveSendExtras(
2198
+ sendExtrasRef.current,
2199
+ ).personalResourceAttachment;
2200
+ const retryInput = composeSendInput(
2201
+ operation.text,
2202
+ nextClientEventId,
2203
+ {
2204
+ ...operation.input,
2205
+ ...(currentPersonalResourceAttachment
2206
+ ? { personalResourceAttachment: currentPersonalResourceAttachment }
2207
+ : {}),
2208
+ },
2209
+ {
2210
+ ...(effectiveControlRef.current?.controlEtag
2211
+ ? { controlEtag: effectiveControlRef.current.controlEtag }
2212
+ : {}),
2213
+ resources: operation.resources,
2214
+ annotations: operation.annotations,
2215
+ },
2216
+ );
1678
2217
  return {
1679
2218
  ...operation,
1680
2219
  clientEventId: nextClientEventId,
1681
2220
  input: retryInput,
1682
- draftPayload: operation.draftPayload
1683
- ? {
1684
- ...operation.draftPayload,
1685
- model: retryExtras.model ?? operation.draftPayload.model,
1686
- reasoningEffort:
1687
- retryExtras.reasoningEffort ?? operation.draftPayload.reasoningEffort,
1688
- latencyMode: retryExtras.latencyMode ?? operation.draftPayload.latencyMode,
1689
- }
1690
- : null,
2221
+ draftPayload: operation.draftPayload,
1691
2222
  occurredAt: new Date().toISOString(),
1692
2223
  state: "sending",
1693
2224
  error: undefined,
2225
+ retryable: undefined,
1694
2226
  outcomeUnknown: false,
1695
2227
  canRetry: true,
1696
2228
  };
@@ -1698,16 +2230,24 @@ export function useComposer(
1698
2230
  );
1699
2231
  queueMicrotask(processOptimisticSends);
1700
2232
  },
1701
- [options.effectiveControl?.controlEtag, processOptimisticSends, replaceOptimisticSends],
2233
+ [dispatch, processOptimisticSends, replaceOptimisticSends],
1702
2234
  );
1703
2235
 
1704
2236
  const removeOptimisticMessage = useCallback(
1705
2237
  (clientEventId: string): void => {
2238
+ if (steeringRef.current?.clientEventId === clientEventId) {
2239
+ pendingOperationRef.current = null;
2240
+ pendingClientEventId.current = null;
2241
+ forgetPendingComposerOperation(pendingComposerOperationKey(workspaceId, sessionId));
2242
+ setSteering(null);
2243
+ setError(null);
2244
+ return;
2245
+ }
1706
2246
  replaceOptimisticSends((current) =>
1707
2247
  current.filter((operation) => operation.clientEventId !== clientEventId),
1708
2248
  );
1709
2249
  },
1710
- [replaceOptimisticSends],
2250
+ [replaceOptimisticSends, sessionId, workspaceId],
1711
2251
  );
1712
2252
 
1713
2253
  // A send is possible with non-empty text OR with ≥1 attached resource (a
@@ -1731,33 +2271,29 @@ export function useComposer(
1731
2271
  setPausing(true);
1732
2272
  setError(null);
1733
2273
  try {
1734
- const result = await client.pauseSession(workspaceId, sessionId, {
1735
- ...(reason !== undefined ? { reason } : {}),
1736
- ...(options.effectiveControl?.controlEtag
1737
- ? { expectedControlEtag: options.effectiveControl.controlEtag }
1738
- : {}),
1739
- });
2274
+ const clientEventId = generateClientEventId();
2275
+ const effectiveControl = effectiveControlRef.current;
2276
+ const result = await replayOutcomeUnknown(() =>
2277
+ client.pauseSession(workspaceId, sessionId, {
2278
+ clientEventId,
2279
+ ...(reason !== undefined ? { reason } : {}),
2280
+ ...(effectiveControl?.controlEtag
2281
+ ? { expectedControlEtag: effectiveControl.controlEtag }
2282
+ : {}),
2283
+ }),
2284
+ );
1740
2285
  if (
1741
2286
  targetKeyRef.current === ownedTargetKey &&
1742
2287
  targetGeneration.current === ownedGeneration
1743
2288
  ) {
1744
- setControlStopping(
1745
- result.interruptionCount > 0 &&
1746
- result.effectiveControl.state === "paused" &&
1747
- result.effectiveControl.settlement !== null
1748
- ? {
1749
- controlVersion: result.effectiveControl.controlVersion,
1750
- controlEtag: result.effectiveControl.controlEtag,
1751
- }
1752
- : null,
1753
- );
2289
+ setAcceptedControl(result.effectiveControl);
1754
2290
  }
1755
2291
  } catch (cause) {
1756
2292
  if (
1757
2293
  targetKeyRef.current === ownedTargetKey &&
1758
2294
  targetGeneration.current === ownedGeneration
1759
2295
  ) {
1760
- setError(cause instanceof Error ? cause : new Error(String(cause)));
2296
+ setError(normalizeError(cause));
1761
2297
  }
1762
2298
  } finally {
1763
2299
  if (
@@ -1768,7 +2304,7 @@ export function useComposer(
1768
2304
  }
1769
2305
  }
1770
2306
  },
1771
- [client, workspaceId, sessionId, pausing, options.effectiveControl?.controlEtag, targetKey],
2307
+ [client, workspaceId, sessionId, pausing, targetKey],
1772
2308
  );
1773
2309
 
1774
2310
  const resume = useCallback(
@@ -1779,18 +2315,29 @@ export function useComposer(
1779
2315
  setResuming(true);
1780
2316
  setError(null);
1781
2317
  try {
1782
- await client.resumeSession(workspaceId, sessionId, {
1783
- ...(reason !== undefined ? { reason } : {}),
1784
- ...(options.effectiveControl?.controlEtag
1785
- ? { expectedControlEtag: options.effectiveControl.controlEtag }
1786
- : {}),
1787
- });
2318
+ const clientEventId = generateClientEventId();
2319
+ const effectiveControl = effectiveControlRef.current;
2320
+ const result = await replayOutcomeUnknown(() =>
2321
+ client.resumeSession(workspaceId, sessionId, {
2322
+ clientEventId,
2323
+ ...(reason !== undefined ? { reason } : {}),
2324
+ ...(effectiveControl?.controlEtag
2325
+ ? { expectedControlEtag: effectiveControl.controlEtag }
2326
+ : {}),
2327
+ }),
2328
+ );
2329
+ if (
2330
+ targetKeyRef.current === ownedTargetKey &&
2331
+ targetGeneration.current === ownedGeneration
2332
+ ) {
2333
+ setAcceptedControl(result.effectiveControl);
2334
+ }
1788
2335
  } catch (cause) {
1789
2336
  if (
1790
2337
  targetKeyRef.current === ownedTargetKey &&
1791
2338
  targetGeneration.current === ownedGeneration
1792
2339
  ) {
1793
- setError(cause instanceof Error ? cause : new Error(String(cause)));
2340
+ setError(normalizeError(cause));
1794
2341
  }
1795
2342
  } finally {
1796
2343
  if (
@@ -1801,7 +2348,7 @@ export function useComposer(
1801
2348
  }
1802
2349
  }
1803
2350
  },
1804
- [client, workspaceId, sessionId, resuming, options.effectiveControl?.controlEtag, targetKey],
2351
+ [client, workspaceId, sessionId, resuming, targetKey],
1805
2352
  );
1806
2353
 
1807
2354
  const resumeScope = useCallback(
@@ -1813,19 +2360,20 @@ export function useComposer(
1813
2360
  setError(null);
1814
2361
  try {
1815
2362
  if (option.scope === "workspace") {
1816
- const workspaceBlocker = options.effectiveControl?.blockers.find(
2363
+ const workspaceBlocker = effectiveControlRef.current?.blockers.find(
1817
2364
  (blocker) => blocker.kind === "workspace",
1818
2365
  );
1819
2366
  if (!client.setWorkspaceInferenceState) {
1820
- throw new Error(
1821
- "@opengeni/react: workspace-scoped resume requires setWorkspaceInferenceState.",
1822
- );
2367
+ throw new ComposerWorkspaceControlUnavailableError();
1823
2368
  }
1824
- await client.setWorkspaceInferenceState(workspaceId, {
1825
- action: "resume",
1826
- clientEventId: generateClientEventId(),
1827
- ...(workspaceBlocker ? { expectedRevision: workspaceBlocker.revision } : {}),
1828
- });
2369
+ const clientEventId = generateClientEventId();
2370
+ await replayOutcomeUnknown(() =>
2371
+ client.setWorkspaceInferenceState!(workspaceId, {
2372
+ action: "resume",
2373
+ clientEventId,
2374
+ ...(workspaceBlocker ? { expectedRevision: workspaceBlocker.revision } : {}),
2375
+ }),
2376
+ );
1829
2377
  } else if (option.scope === "session" && option.targetId) {
1830
2378
  const target = await client.getQueue(workspaceId, option.targetId);
1831
2379
  if (
@@ -1834,15 +2382,30 @@ export function useComposer(
1834
2382
  ) {
1835
2383
  return;
1836
2384
  }
1837
- await client.resumeSession(workspaceId, option.targetId, {
1838
- expectedControlEtag: target.effectiveControl.controlEtag,
1839
- });
2385
+ const clientEventId = generateClientEventId();
2386
+ await replayOutcomeUnknown(() =>
2387
+ client.resumeSession(workspaceId, option.targetId!, {
2388
+ clientEventId,
2389
+ expectedControlEtag: target.effectiveControl.controlEtag,
2390
+ }),
2391
+ );
1840
2392
  } else {
1841
- await client.resumeSession(workspaceId, sessionId, {
1842
- ...(options.effectiveControl?.controlEtag
1843
- ? { expectedControlEtag: options.effectiveControl.controlEtag }
1844
- : {}),
1845
- });
2393
+ const clientEventId = generateClientEventId();
2394
+ const effectiveControl = effectiveControlRef.current;
2395
+ const result = await replayOutcomeUnknown(() =>
2396
+ client.resumeSession(workspaceId, sessionId, {
2397
+ clientEventId,
2398
+ ...(effectiveControl?.controlEtag
2399
+ ? { expectedControlEtag: effectiveControl.controlEtag }
2400
+ : {}),
2401
+ }),
2402
+ );
2403
+ if (
2404
+ targetKeyRef.current === ownedTargetKey &&
2405
+ targetGeneration.current === ownedGeneration
2406
+ ) {
2407
+ setAcceptedControl(result.effectiveControl);
2408
+ }
1846
2409
  }
1847
2410
  } catch (cause) {
1848
2411
  if (
@@ -1860,7 +2423,7 @@ export function useComposer(
1860
2423
  }
1861
2424
  }
1862
2425
  },
1863
- [client, options.effectiveControl, resuming, sessionId, targetKey, workspaceId],
2426
+ [client, resuming, sessionId, targetKey, workspaceId],
1864
2427
  );
1865
2428
 
1866
2429
  const updateValue = useCallback(
@@ -1878,6 +2441,7 @@ export function useComposer(
1878
2441
  resolveSendExtras(sendExtrasRef.current).resources ?? [],
1879
2442
  ),
1880
2443
  annotations: annotationsRef.current,
2444
+ policy: policyRef.current ?? undefined,
1881
2445
  },
1882
2446
  );
1883
2447
  setOptimisticDraftShadow({
@@ -1887,6 +2451,7 @@ export function useComposer(
1887
2451
  resolveSendExtras(sendExtrasRef.current).resources ?? [],
1888
2452
  ),
1889
2453
  annotations: annotationsRef.current,
2454
+ policy: policyRef.current ?? undefined,
1890
2455
  });
1891
2456
  setValue(next);
1892
2457
  },
@@ -1908,6 +2473,7 @@ export function useComposer(
1908
2473
  resolveSendExtras(sendExtrasRef.current).resources ?? [],
1909
2474
  ),
1910
2475
  annotations: next,
2476
+ policy: policyRef.current ?? undefined,
1911
2477
  },
1912
2478
  );
1913
2479
  setOptimisticDraftShadow({
@@ -1917,6 +2483,7 @@ export function useComposer(
1917
2483
  resolveSendExtras(sendExtrasRef.current).resources ?? [],
1918
2484
  ),
1919
2485
  annotations: next,
2486
+ policy: policyRef.current ?? undefined,
1920
2487
  });
1921
2488
  setAnnotations(next);
1922
2489
  setAnnotationReviewTargetId(reviewTargetId);
@@ -1927,7 +2494,7 @@ export function useComposer(
1927
2494
  const addAnnotation = useCallback(
1928
2495
  (annotation: DraftTimelineAnnotation) => {
1929
2496
  if (annotationsRef.current.length >= 12) {
1930
- setError(new Error("A message can include at most 12 timeline annotations."));
2497
+ setError(new ComposerStateError("A message can include at most 12 timeline annotations."));
1931
2498
  return;
1932
2499
  }
1933
2500
  const next = [...annotationsRef.current, annotation];
@@ -1958,6 +2525,69 @@ export function useComposer(
1958
2525
  setAnnotationReviewTargetId(null);
1959
2526
  }, []);
1960
2527
 
2528
+ const requestAnnotationReview = useCallback((id?: string) => {
2529
+ const current = annotationsRef.current;
2530
+ if (current.length === 0) {
2531
+ setAnnotationReviewTargetId(null);
2532
+ return;
2533
+ }
2534
+ if (id && current.some((annotation) => annotation.id === id)) {
2535
+ setAnnotationReviewTargetId(id);
2536
+ return;
2537
+ }
2538
+ const incomplete = current.find((annotation) => annotation.note.trim().length === 0);
2539
+ setAnnotationReviewTargetId(incomplete?.id ?? current[0]!.id);
2540
+ }, []);
2541
+
2542
+ const updatePolicy = useCallback(
2543
+ (next: ComposerPolicy): void => {
2544
+ if (targetKeyRef.current !== targetKey) return;
2545
+ localEditRevision.current += 1;
2546
+ policyRef.current = next;
2547
+ const shadow = {
2548
+ text: valueRef.current,
2549
+ resources: mergeResources(
2550
+ restoredResourcesRef.current,
2551
+ resolveSendExtras(sendExtrasRef.current).resources ?? [],
2552
+ ),
2553
+ annotations: annotationsRef.current,
2554
+ policy: next,
2555
+ };
2556
+ pendingOperationRef.current = updatePendingComposerShadow(
2557
+ pendingOperationKey,
2558
+ pendingOperationRef.current,
2559
+ shadow,
2560
+ );
2561
+ setOptimisticDraftShadow(shadow);
2562
+ setPolicy(next);
2563
+ },
2564
+ [pendingOperationKey, setOptimisticDraftShadow, targetKey],
2565
+ );
2566
+
2567
+ const updateModel = useCallback(
2568
+ (model: string): void => {
2569
+ const current = policyRef.current;
2570
+ if (current) updatePolicy({ ...current, model });
2571
+ },
2572
+ [updatePolicy],
2573
+ );
2574
+
2575
+ const updateReasoningEffort = useCallback(
2576
+ (reasoningEffort: ReasoningEffort): void => {
2577
+ const current = policyRef.current;
2578
+ if (current) updatePolicy({ ...current, reasoningEffort });
2579
+ },
2580
+ [updatePolicy],
2581
+ );
2582
+
2583
+ const updateLatencyMode = useCallback(
2584
+ (latencyMode: LatencyMode): void => {
2585
+ const current = policyRef.current;
2586
+ if (current) updatePolicy({ ...current, latencyMode });
2587
+ },
2588
+ [updatePolicy],
2589
+ );
2590
+
1961
2591
  const removeRestoredResource = useCallback(
1962
2592
  (index: number) => {
1963
2593
  if (targetKeyRef.current !== targetKey) return;
@@ -1971,12 +2601,14 @@ export function useComposer(
1971
2601
  text: valueRef.current,
1972
2602
  resources: mergeResources(next, resolveSendExtras(sendExtrasRef.current).resources ?? []),
1973
2603
  annotations: annotationsRef.current,
2604
+ policy: policyRef.current ?? undefined,
1974
2605
  },
1975
2606
  );
1976
2607
  setOptimisticDraftShadow({
1977
2608
  text: valueRef.current,
1978
2609
  resources: mergeResources(next, resolveSendExtras(sendExtrasRef.current).resources ?? []),
1979
2610
  annotations: annotationsRef.current,
2611
+ policy: policyRef.current ?? undefined,
1980
2612
  });
1981
2613
  setRestoredResources(next);
1982
2614
  },
@@ -1995,6 +2627,42 @@ export function useComposer(
1995
2627
  );
1996
2628
  }, []);
1997
2629
 
2630
+ const restoreOptimisticMessage = useCallback(
2631
+ (clientEventId: string): void => {
2632
+ if (
2633
+ targetKeyRef.current !== targetKey ||
2634
+ draftLoading ||
2635
+ pendingOperationRef.current ||
2636
+ hasDraftContent()
2637
+ )
2638
+ return;
2639
+ const operation = optimisticSendsRef.current.find(
2640
+ (candidate) => candidate.clientEventId === clientEventId,
2641
+ );
2642
+ if (!operation || operation.state !== "failed" || operation.outcomeUnknown) return;
2643
+ // Restoring is a new draft edit, not a retry. Keep the user's current model
2644
+ // selection and never copy credentials from the old rejected request.
2645
+ localEditRevision.current += 1;
2646
+ valueRef.current = operation.text;
2647
+ restoredResourcesRef.current = [...operation.resources];
2648
+ annotationsRef.current = cloneAnnotations(operation.annotations);
2649
+ setValue(operation.text);
2650
+ setRestoredResources(restoredResourcesRef.current);
2651
+ setAnnotations(annotationsRef.current);
2652
+ setError(null);
2653
+ setOptimisticDraftShadow({
2654
+ text: operation.text,
2655
+ resources: restoredResourcesRef.current,
2656
+ annotations: annotationsRef.current,
2657
+ policy: policyRef.current ?? undefined,
2658
+ });
2659
+ replaceOptimisticSends((current) =>
2660
+ current.filter((candidate) => candidate.clientEventId !== clientEventId),
2661
+ );
2662
+ },
2663
+ [draftLoading, hasDraftContent, replaceOptimisticSends, setOptimisticDraftShadow, targetKey],
2664
+ );
2665
+
1998
2666
  const resolveDraftConflict = useCallback(
1999
2667
  async (choice: "keep_mine" | "use_remote"): Promise<void> => {
2000
2668
  const ownedTargetKey = targetKey;
@@ -2030,15 +2698,62 @@ export function useComposer(
2030
2698
 
2031
2699
  const identityMatches = stateTargetKey === targetKey;
2032
2700
  const visibleSteering = identityMatches ? steering : null;
2701
+ const visibleOptimisticMessages: ComposerOptimisticMessage[] = identityMatches
2702
+ ? [
2703
+ ...optimisticSends.map(
2704
+ ({ input: _input, draftPayload: _payload, canRetry: _retry, ...item }) => item,
2705
+ ),
2706
+ ...(visibleSteering?.clientEventId
2707
+ ? [
2708
+ {
2709
+ clientEventId: visibleSteering.clientEventId,
2710
+ delivery: "steer" as const,
2711
+ destination: "chat" as const,
2712
+ text: visibleSteering.text,
2713
+ annotations:
2714
+ pendingOperationRef.current?.delivery === "steer"
2715
+ ? pendingOperationRef.current.annotationsAtSend
2716
+ : [],
2717
+ resources:
2718
+ pendingOperationRef.current?.delivery === "steer"
2719
+ ? pendingOperationRef.current.resourcesAtSend
2720
+ : [],
2721
+ occurredAt: steeringOccurredAtRef.current,
2722
+ state:
2723
+ visibleSteering.phase === "submitting"
2724
+ ? ("sending" as const)
2725
+ : visibleSteering.phase === "failed"
2726
+ ? ("failed" as const)
2727
+ : ("queued" as const),
2728
+ turnId: visibleSteering.turnId,
2729
+ triggerEventId: visibleSteering.triggerEventId,
2730
+ ...(visibleSteering.error ? { error: visibleSteering.error } : {}),
2731
+ ...(visibleSteering.outcomeUnknown !== undefined
2732
+ ? { outcomeUnknown: visibleSteering.outcomeUnknown }
2733
+ : {}),
2734
+ },
2735
+ ]
2736
+ : []),
2737
+ ]
2738
+ : [];
2033
2739
  const reloadDraft = useCallback(async () => await loadDraft(true), [loadDraft]);
2034
2740
  const clearError = useCallback(() => {
2035
2741
  if (targetKeyRef.current !== targetKey) return;
2036
2742
  setError(null);
2743
+ setDraftReadError(null);
2037
2744
  setDraftConflict(null);
2038
2745
  }, [targetKey]);
2746
+ // `valueRef` is the synchronous composer authority. React state exists to
2747
+ // schedule renders, but a concurrent autosave settlement can render the
2748
+ // previous state lane after a newer input event has already updated the ref.
2749
+ // Projecting that stale state into a controlled textarea rewrites the old
2750
+ // draft for one commit, which moves the caret and can briefly resurrect a
2751
+ // submitted message. Never let an incidental render outrank the latest
2752
+ // local lifecycle decision.
2753
+ const visibleValue = identityMatches ? valueRef.current : "";
2039
2754
 
2040
2755
  return {
2041
- value: identityMatches ? value : "",
2756
+ value: visibleValue,
2042
2757
  setValue: updateValue,
2043
2758
  annotations: identityMatches ? annotations : [],
2044
2759
  addAnnotation,
@@ -2046,33 +2761,34 @@ export function useComposer(
2046
2761
  removeAnnotation,
2047
2762
  annotationReviewTargetId: identityMatches ? annotationReviewTargetId : null,
2048
2763
  clearAnnotationReviewTarget,
2764
+ requestAnnotationReview,
2049
2765
  hasDraftContent,
2050
2766
  send,
2051
- optimisticMessages: identityMatches
2052
- ? optimisticSends.map(
2053
- ({ input: _input, draftPayload: _payload, canRetry: _retry, ...item }) => item,
2054
- )
2055
- : [],
2767
+ optimisticMessages: visibleOptimisticMessages,
2056
2768
  retryOptimisticMessage,
2769
+ restoreOptimisticMessage:
2770
+ !draftLoading && !pendingOperationRef.current && !hasDraftContent()
2771
+ ? restoreOptimisticMessage
2772
+ : undefined,
2057
2773
  removeOptimisticMessage,
2058
2774
  steer,
2059
2775
  steering: visibleSteering,
2060
2776
  stoppingAttempt: identityMatches
2061
- ? controlStopping
2062
- ? "current"
2063
- : visibleSteering?.phase === "accepted" && visibleSteering.stoppingPreviousAttempt === true
2064
- ? "previous"
2065
- : null
2777
+ ? visibleSteering?.phase === "accepted" && visibleSteering.stoppingPreviousAttempt === true
2778
+ ? "previous"
2779
+ : null
2066
2780
  : null,
2067
2781
  sending: identityMatches ? sending : false,
2068
2782
  canSend:
2069
2783
  identityMatches &&
2070
2784
  Boolean(sessionId) &&
2071
2785
  !sending &&
2786
+ !draftLoading &&
2787
+ policy !== null &&
2072
2788
  sendBlockedRef.current?.() !== true &&
2073
2789
  annotationsComplete &&
2074
2790
  (hasPendingOperation ||
2075
- value.trim().length > 0 ||
2791
+ visibleValue.trim().length > 0 ||
2076
2792
  hasReadyResources ||
2077
2793
  annotations.length > 0),
2078
2794
  pause,
@@ -2080,18 +2796,23 @@ export function useComposer(
2080
2796
  resume,
2081
2797
  resumeScope,
2082
2798
  resuming: identityMatches ? resuming : false,
2799
+ effectiveControl: identityMatches ? effectiveControlRef.current : null,
2083
2800
  draft: identityMatches ? draft : null,
2084
2801
  draftRevision: identityMatches ? (draft?.revision ?? 0) : 0,
2085
2802
  draftLoading: identityMatches ? draftLoading : Boolean(sessionId) && durableDrafts,
2086
2803
  draftSaving: identityMatches ? draftSaving : false,
2087
2804
  draftConflict: identityMatches ? draftConflict : null,
2805
+ policy: identityMatches ? policy : null,
2806
+ setModel: updateModel,
2807
+ setReasoningEffort: updateReasoningEffort,
2808
+ setLatencyMode: updateLatencyMode,
2088
2809
  draftPersistence: durableDrafts ? "durable" : "disabled",
2089
2810
  applyDraft,
2090
2811
  reloadDraft,
2091
2812
  resolveDraftConflict,
2092
2813
  restoredResources: identityMatches ? restoredResources : [],
2093
2814
  removeRestoredResource,
2094
- error: identityMatches ? error : null,
2815
+ error: identityMatches ? (error ?? draftReadError) : null,
2095
2816
  clearError,
2096
2817
  };
2097
2818
  }
@@ -2160,58 +2881,42 @@ function isOutcomeUnknownError(cause: unknown): boolean {
2160
2881
  );
2161
2882
  }
2162
2883
 
2884
+ async function replayOutcomeUnknown<T>(command: () => Promise<T>): Promise<T> {
2885
+ try {
2886
+ return await command();
2887
+ } catch (cause) {
2888
+ if (!isOutcomeUnknownError(cause)) throw cause;
2889
+ return await command();
2890
+ }
2891
+ }
2892
+
2163
2893
  function asError(cause: unknown): Error {
2164
- return cause instanceof Error ? cause : new Error(String(cause));
2894
+ return normalizeError(cause);
2165
2895
  }
2166
2896
 
2167
- function isDraftConflictError(error: Error): boolean {
2168
- const apiError = error as Partial<OpenGeniApiError>;
2169
- if (apiError.status !== 409 || apiError.outcomeUnknown === true) return false;
2170
- // Production queue OCC returns `DRAFT_CHANGED`. Older/SDK-shaped 409s may
2171
- // omit code or use the generic conflict labels — all are recoverable OCC.
2172
- const code = apiError.code;
2897
+ function promptRoutingFromAcceptedEvent(
2898
+ event: SessionEvent | null,
2899
+ ): "accepted_for_execution" | "queued_for_execution" | "accepted_for_steering" | null {
2173
2900
  if (
2174
- code === undefined ||
2175
- code === "DRAFT_CHANGED" ||
2176
- code === "conflict" ||
2177
- code === "idempotency_conflict"
2901
+ typeof event?.payload !== "object" ||
2902
+ event.payload === null ||
2903
+ Array.isArray(event.payload)
2178
2904
  ) {
2179
- return true;
2905
+ return null;
2180
2906
  }
2181
- return /draft changed/i.test(error.message);
2907
+ const routing = (event.payload as Record<string, unknown>).routing;
2908
+ return routing === "accepted_for_execution" ||
2909
+ routing === "queued_for_execution" ||
2910
+ routing === "accepted_for_steering"
2911
+ ? routing
2912
+ : null;
2182
2913
  }
2183
2914
 
2184
- /**
2185
- * One OCC retry: adopt the server revision and rewrite the same local content.
2186
- * Covers the common "tab slept through a successful autosave" case without
2187
- * stranding the operator on a raw 409 toast.
2188
- */
2189
- async function saveComposerDraftWithStaleRetry(input: {
2190
- client: {
2191
- getComposerDraft: (workspaceId: string, sessionId: string) => Promise<ComposerDraft>;
2192
- saveComposerDraft: (
2193
- workspaceId: string,
2194
- sessionId: string,
2195
- request: SaveComposerDraftRequest,
2196
- ) => Promise<ComposerDraft>;
2197
- };
2198
- workspaceId: string;
2199
- sessionId: string;
2200
- request: SaveComposerDraftRequest;
2201
- onAdoptRemote: (remote: ComposerDraft) => void;
2202
- }): Promise<ComposerDraft> {
2203
- try {
2204
- return await input.client.saveComposerDraft(input.workspaceId, input.sessionId, input.request);
2205
- } catch (cause) {
2206
- const problem = asError(cause);
2207
- if (!isDraftConflictError(problem)) throw problem;
2208
- const remote = await input.client.getComposerDraft(input.workspaceId, input.sessionId);
2209
- input.onAdoptRemote(remote);
2210
- return await input.client.saveComposerDraft(input.workspaceId, input.sessionId, {
2211
- ...input.request,
2212
- expectedRevision: remote.revision,
2213
- });
2214
- }
2915
+ function isDraftConflictError(error: Error): boolean {
2916
+ const apiError = error as Partial<OpenGeniApiError>;
2917
+ return (
2918
+ apiError.status === 409 && apiError.outcomeUnknown !== true && apiError.code === "DRAFT_CHANGED"
2919
+ );
2215
2920
  }
2216
2921
 
2217
2922
  function draftPayload(draft: ComposerDraft): SaveComposerDraftRequest {
@@ -2222,7 +2927,15 @@ function draftPayload(draft: ComposerDraft): SaveComposerDraftRequest {
2222
2927
  annotations: draft.annotations ?? [],
2223
2928
  model: draft.model,
2224
2929
  reasoningEffort: draft.reasoningEffort,
2225
- latencyMode: draft.latencyMode ?? "standard",
2930
+ latencyMode: draft.latencyMode,
2931
+ };
2932
+ }
2933
+
2934
+ function policyFromDraft(draft: ComposerDraft): ComposerPolicy {
2935
+ return {
2936
+ model: draft.model,
2937
+ reasoningEffort: draft.reasoningEffort,
2938
+ latencyMode: draft.latencyMode,
2226
2939
  };
2227
2940
  }
2228
2941
 
@@ -2231,16 +2944,17 @@ function composerDraftPayload(
2231
2944
  text: string,
2232
2945
  restoredResources: ResourceRef[],
2233
2946
  annotations: DraftTimelineAnnotation[],
2234
- extras: ComposerSendExtras,
2947
+ policy: ComposerPolicy,
2948
+ additionalResources: ResourceRef[],
2235
2949
  ): SaveComposerDraftRequest {
2236
2950
  return {
2237
2951
  expectedRevision: base.revision,
2238
2952
  text,
2239
- resources: mergeResources(restoredResources, extras.resources ?? []),
2953
+ resources: mergeResources(restoredResources, additionalResources),
2240
2954
  annotations,
2241
- model: extras.model ?? base.model,
2242
- reasoningEffort: extras.reasoningEffort ?? base.reasoningEffort,
2243
- latencyMode: extras.latencyMode ?? base.latencyMode ?? "standard",
2955
+ model: policy.model,
2956
+ reasoningEffort: policy.reasoningEffort,
2957
+ latencyMode: policy.latencyMode,
2244
2958
  };
2245
2959
  }
2246
2960