@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
@@ -1,13 +1,23 @@
1
1
  import {
2
2
  parseMediaGenerationResult,
3
+ parseToolDisplayMetadata,
4
+ EMPTY_FINAL_REPLY_NOTICE,
5
+ turnCompletedWithEmptyFinalReply,
6
+ type HumanInputAnswer,
7
+ type HumanInputQuestion,
8
+ type HumanInputResponse,
3
9
  type SessionEvent,
4
10
  type SessionStatus,
5
11
  type TimelineAnnotation,
6
12
  } from "@opengeni/sdk";
7
13
  import fleetDecisionItem from "./fleet-decision-projection";
14
+ import {
15
+ allowanceExhaustedMessage,
16
+ parseAllowanceExhaustedRefusal,
17
+ } from "@opengeni/sdk/allowance-refusal";
8
18
  import {
9
19
  CREDIT_EXHAUSTION_MESSAGE,
10
- humanizeFailureReason,
20
+ presentFailure,
11
21
  isCreditExhaustion,
12
22
  tryParseJson,
13
23
  } from "../lib/format";
@@ -18,9 +28,15 @@ import type {
18
28
  AuthNeededItem,
19
29
  ContextCompactionItem,
20
30
  GoalItem,
31
+ HumanInputAnswerSummary,
32
+ HumanInputItem,
21
33
  MachineInputBatchItem,
34
+ MachineInputMember,
22
35
  MemoryItem,
36
+ NoticeItem,
23
37
  SandboxItem,
38
+ StartupPhase,
39
+ StartupPhaseItem,
24
40
  SessionStatusItem,
25
41
  TimelineGroup,
26
42
  TimelineItem,
@@ -32,6 +48,14 @@ import type {
32
48
 
33
49
  export { toolDisplayName, mcpToolLeaf, toolMatchesLeaf } from "./tool-display-name";
34
50
 
51
+ /** Message identity only: queue placement needs the surrounding turn history. */
52
+ export function isTimelineUserQuestion(event: SessionEvent): boolean {
53
+ return (
54
+ event.type === "user.message" &&
55
+ workerCompletionPayload(asRecord(event.payload).childCompletion) === null
56
+ );
57
+ }
58
+
35
59
  /* ----------------------------------------------------------------------------
36
60
  Timeline projection
37
61
 
@@ -51,6 +75,8 @@ const WORKER_SPAWN_TOOL = "session_create";
51
75
  const WORKER_MESSAGE_TOOL = "session_send_message";
52
76
  const WORKER_FAILURE_CODE_MAX_LENGTH = 128;
53
77
  const WORKER_FAILURE_MESSAGE_MAX_UTF8_BYTES = 1_024;
78
+ type PendingWaitOutcome = { id: string; reason: string; occurredAt: string };
79
+ type TrackedAgentResponse = { item: AgentMessageItem; completed: boolean };
54
80
 
55
81
  /**
56
82
  * Tools whose durable side-effect events already own the timeline (MemoryRow).
@@ -120,13 +146,199 @@ function timelineAnnotationToolOutputText(output: unknown): string | null {
120
146
  return text.length > 0 ? text : null;
121
147
  }
122
148
 
123
- export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
149
+ export function buildTimeline(
150
+ events: SessionEvent[],
151
+ options: { partialStart?: boolean } = {},
152
+ ): TimelineItem[] {
124
153
  const items: TimelineItem[] = [];
154
+ const allowanceNotices = new Set<string>();
155
+ const allowanceNotice = (event: SessionEvent, turnId: string | null) => {
156
+ const refusal = parseAllowanceExhaustedRefusal(event.payload);
157
+ if (!refusal) return null;
158
+ const text = allowanceExhaustedMessage(refusal);
159
+ const key = JSON.stringify([
160
+ turnId,
161
+ event.turnAttemptId,
162
+ startupRecoveryRevisionByTurn.get(turnId ?? "") ?? 0,
163
+ refusal.scope,
164
+ refusal.subjectId,
165
+ refusal.resetsAt,
166
+ ]);
167
+ if (!allowanceNotices.has(key)) {
168
+ allowanceNotices.add(key);
169
+ items.push({
170
+ kind: "notice",
171
+ id: `${event.id}-allowance`,
172
+ tone: "failed",
173
+ text,
174
+ // The canonical sentence replaces upstream prose, so a host renderer
175
+ // reading `message` never shows a wrapper's private wording.
176
+ allowance: { ...refusal, message: text },
177
+ occurredAt: event.occurredAt,
178
+ });
179
+ }
180
+ return text;
181
+ };
125
182
  const prescan = prescanTurnAnchors(events);
126
183
  const ordered = orderTimelineEvents(events, prescan);
184
+ // A bounded replay can begin halfway through a message (including inside an
185
+ // interactive fence). Never parse that suffix as a complete Markdown source.
186
+ // Keep the raw events available for pagination; a completed receipt restores
187
+ // the authoritative message without requiring an unbounded history fetch.
188
+ let completeHistoryPrefix = options.partialStart !== true;
189
+ const knownMessagePrefixTurns = new Set<string | null>();
190
+ const incompleteMessageKeys = new Set<string>();
191
+ const pendingWaitOutcomeByTurn = new Map<string | null, PendingWaitOutcome>();
192
+ const latestAgentResponseByTurn = new Map<string | null, TrackedAgentResponse>();
193
+ const identifiedMessages = new Map<string, AgentMessageItem>();
194
+ const humanInputRequests = humanInputRequestsById(events);
195
+ const humanInputToolCallIds = new Set(
196
+ [...humanInputRequests.values()]
197
+ .map((request) => stringValue(request.toolCallId))
198
+ .filter(Boolean),
199
+ );
200
+ // Delegated workers that have not reported back yet, so a recorded agent wait
201
+ // can say how many agents it is waiting for. Only a session's own workers
202
+ // report back, so a message to any other session is never awaited. Best
203
+ // effort: a bounded replay only knows the spawns and results inside its window.
204
+ const workerSessions = new Set<string>();
205
+ const pendingWorkerSessions = new Set<string>();
206
+ let openWaitNotice: NoticeItem | null = null;
207
+ // Tool results update the original tool row in place, and running lifecycle
208
+ // receipts have no row. Carry resume evidence on the attention landmarks so
209
+ // grouping cannot mistake historical approval for a live wait.
210
+ const presentationWaits = new Map<string | null, Array<NoticeItem | SessionStatusItem>>();
211
+ const presentationFailures = new Map<string | null, SessionStatusItem[]>();
212
+ let presentationTurnId: string | null = null;
213
+ const rememberPresentationWait = (
214
+ item: NoticeItem | SessionStatusItem,
215
+ turnId: string | null,
216
+ ) => {
217
+ const key = turnId ?? presentationTurnId;
218
+ const waits = presentationWaits.get(key) ?? [];
219
+ waits.push(item);
220
+ presentationWaits.set(key, waits);
221
+ items.push(item);
222
+ };
223
+ const resolvePresentationWait = (event: SessionEvent, turnId: string | null) => {
224
+ if (event.duplicateOfEventId || (event.turnAssociation && event.turnAssociation !== "current"))
225
+ return;
226
+ const key = turnId ?? presentationTurnId;
227
+ for (const item of presentationWaits.get(key) ?? []) item.resolvedAt = event.occurredAt;
228
+ presentationWaits.delete(key);
229
+ };
230
+ const queuedAtByTurn = new Map<string, string>();
231
+ const startupRecoveryRevisionByTurn = new Map<string, number>();
232
+ const startupPhases = new Map<string, readonly [StartupPhaseItem, string | null, number]>();
233
+ let startupEvent: SessionEvent;
234
+ let startupTurnId: string | null = null;
235
+ let startupAttemptId: string | null = null;
236
+ let startupRecoveryRevision = 0;
237
+
238
+ const settleStartupPhase = (
239
+ phase: StartupPhase,
240
+ status: StartupPhaseItem["status"],
241
+ duration: number | null,
242
+ outcome: StartupPhaseItem["outcome"],
243
+ mode = 0,
244
+ explicitStartedAt?: string,
245
+ explicitId?: string,
246
+ blockedReason?: StartupPhaseItem["blockedReason"],
247
+ ): void => {
248
+ const key = `${startupTurnId}:${phase}`;
249
+ const priorState = startupPhases.get(key);
250
+ const prior = priorState?.[0];
251
+ const comparedByAttempt = priorState?.[1] && startupAttemptId;
252
+ const sameAttempt = comparedByAttempt
253
+ ? priorState[1] === startupAttemptId
254
+ : (priorState?.[2] ?? 0) === startupRecoveryRevision;
255
+ const open = prior?.status === "running" && sameAttempt ? prior : undefined;
256
+ const replacesPriorAttempt =
257
+ prior !== undefined &&
258
+ (comparedByAttempt ? !sameAttempt : startupRecoveryRevision > (priorState?.[2] ?? 0));
259
+ // A later activity attempt may replay preparation that the logical turn had
260
+ // already completed before the worker moved. Keep that first successful
261
+ // landmark; only an abandoned or failed attempt needs replacement.
262
+ if (replacesPriorAttempt && prior.status === "complete") return;
263
+ if (mode === 2 && prior && !replacesPriorAttempt) return;
264
+ if (mode === 1 && !open) return;
265
+ if (!replacesPriorAttempt && status !== "running" && open) {
266
+ open.status = status;
267
+ open.completedAt = startupEvent.occurredAt;
268
+ open.durationMs =
269
+ duration === null
270
+ ? elapsedDurationMs(open.startedAt, startupEvent.occurredAt)
271
+ : Math.max(0, duration);
272
+ open.outcome = outcome ?? open.outcome;
273
+ open.blockedReason = blockedReason;
274
+ return;
275
+ }
276
+ const running = status === "running";
277
+ const durationMs = running || duration === null ? null : Math.max(0, duration);
278
+ const completedEpoch = Date.parse(startupEvent.occurredAt);
279
+ const startedAt =
280
+ explicitStartedAt ??
281
+ (!running && durationMs !== null && Number.isFinite(completedEpoch)
282
+ ? new Date(completedEpoch - durationMs).toISOString()
283
+ : startupEvent.occurredAt);
284
+ if (replacesPriorAttempt) {
285
+ Object.assign(prior, {
286
+ id: explicitId ?? startupEvent.id,
287
+ status,
288
+ startedAt,
289
+ completedAt: running ? null : startupEvent.occurredAt,
290
+ durationMs,
291
+ outcome,
292
+ blockedReason,
293
+ occurredAt: startedAt,
294
+ } satisfies Partial<StartupPhaseItem>);
295
+ startupPhases.set(key, [prior, startupAttemptId, startupRecoveryRevision]);
296
+ return;
297
+ }
298
+ const item: StartupPhaseItem = {
299
+ kind: "startup-phase",
300
+ id: explicitId ?? startupEvent.id,
301
+ turnId: startupTurnId,
302
+ phase,
303
+ status,
304
+ startedAt,
305
+ completedAt: running ? null : startupEvent.occurredAt,
306
+ durationMs,
307
+ outcome,
308
+ blockedReason,
309
+ occurredAt: startedAt,
310
+ };
311
+ items.push(item);
312
+ startupPhases.set(key, [item, startupAttemptId, startupRecoveryRevision]);
313
+ };
127
314
 
128
315
  const last = (): TimelineItem | undefined => items[items.length - 1];
129
316
 
317
+ const rememberAgentResponse = (
318
+ turnId: string | null,
319
+ item: AgentMessageItem,
320
+ completed: boolean,
321
+ ): void => {
322
+ latestAgentResponseByTurn.set(turnId, { item, completed });
323
+ };
324
+
325
+ const takePendingWaitOutcome = (turnId: string | null): PendingWaitOutcome | undefined => {
326
+ const outcome = pendingWaitOutcomeByTurn.get(turnId);
327
+ pendingWaitOutcomeByTurn.delete(turnId);
328
+ return outcome;
329
+ };
330
+
331
+ const takeAgentResponse = (turnId: string | null): TrackedAgentResponse | undefined => {
332
+ const response = latestAgentResponseByTurn.get(turnId);
333
+ latestAgentResponseByTurn.delete(turnId);
334
+ return response;
335
+ };
336
+
337
+ const clearUnscopedTerminalTracking = (): void => {
338
+ latestAgentResponseByTurn.delete(null);
339
+ pendingWaitOutcomeByTurn.delete(null);
340
+ };
341
+
130
342
  /** A new item of a different kind ends whatever was streaming at the tail. */
131
343
  const closeStreamingTail = (): void => {
132
344
  const open = last();
@@ -138,6 +350,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
138
350
  const finalizeOpen = (
139
351
  turnId?: string | null,
140
352
  disposition: "complete" | "failed" | "cancelled" = "complete",
353
+ completedAt?: string,
141
354
  ): void => {
142
355
  for (const item of items) {
143
356
  if (
@@ -158,20 +371,119 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
158
371
  if (item.kind === "sandbox" && item.status === "running") {
159
372
  item.status = disposition;
160
373
  }
374
+ if (item.kind === "startup-phase" && item.status === "running") {
375
+ item.status = disposition;
376
+ if (completedAt) {
377
+ item.completedAt = completedAt;
378
+ item.durationMs = elapsedDurationMs(item.startedAt, completedAt);
379
+ }
380
+ }
161
381
  }
162
382
  };
163
383
 
164
384
  for (const event of ordered) {
165
385
  const payload = asRecord(event.payload);
166
386
  const turnId = event.turnId ?? null;
387
+ if (
388
+ turnId &&
389
+ (event.type === "turn.started" ||
390
+ (presentationTurnId === null && isAgentActivityEvent(event.type)))
391
+ ) {
392
+ presentationTurnId = turnId;
393
+ }
394
+ if (
395
+ openWaitNotice &&
396
+ (event.type === "user.message" ||
397
+ event.type === "system.update.delivered" ||
398
+ event.type === "turn.started" ||
399
+ event.type === "session.control.paused" ||
400
+ (event.type === "session.status.changed" &&
401
+ (payload.status === "cancelled" || payload.status === "failed")))
402
+ ) {
403
+ // New input, a pause, or the end of the session ends a recorded wait; the
404
+ // notice keeps its wait span.
405
+ openWaitNotice.waitEndedAt = event.occurredAt;
406
+ openWaitNotice = null;
407
+ }
408
+ if (event.type === "session.created") {
409
+ completeHistoryPrefix = true;
410
+ } else if (
411
+ event.type === "turn.started" ||
412
+ (event.type === "user.message" &&
413
+ payload.delivery !== "steer" &&
414
+ payload.routing !== "accepted_for_steering")
415
+ ) {
416
+ // Accepted Steer is visible before its replacement turn starts. It says
417
+ // nothing about the prefix of a still-streaming message from the old turn.
418
+ knownMessagePrefixTurns.add(turnId);
419
+ }
420
+ startupEvent = event;
421
+ startupTurnId = turnId;
422
+ startupAttemptId =
423
+ (typeof event.turnAttemptId === "string" ? event.turnAttemptId : null) ??
424
+ stringValue(payload.attemptId);
425
+ startupRecoveryRevision = turnId ? (startupRecoveryRevisionByTurn.get(turnId) ?? 0) : 0;
426
+ const setupPhase = event.type.startsWith("rig.setup.")
427
+ ? "rig"
428
+ : event.type.startsWith("turn.startup.phase.")
429
+ ? startupPhaseFromPayload(payload.phase)
430
+ : null;
431
+ const setupStatus = event.type.endsWith(".started")
432
+ ? "running"
433
+ : event.type.endsWith(".failed")
434
+ ? "failed"
435
+ : event.type.endsWith(".completed") || event.type === "rig.setup.skipped"
436
+ ? "complete"
437
+ : null;
438
+ if (setupPhase && setupStatus) {
439
+ closeStreamingTail();
440
+ let blockedReason: StartupPhaseItem["blockedReason"];
441
+ if (setupPhase === "model_preparation" && setupStatus === "failed") {
442
+ blockedReason = startupLifecycleWaitReason(payload);
443
+ // Older preparation events omit the typed lifecycle fields. The
444
+ // sandbox span can supply them only inside the same turn attempt.
445
+ const sandbox = startupPhases.get(`${startupTurnId}:sandbox`);
446
+ const sameAttempt =
447
+ sandbox?.[1] || startupAttemptId
448
+ ? Boolean(startupAttemptId && sandbox?.[1] === startupAttemptId)
449
+ : sandbox?.[2] === startupRecoveryRevision;
450
+ if (
451
+ !blockedReason &&
452
+ payload.expectedTransition === undefined &&
453
+ payload.failureCode === undefined &&
454
+ payload.failureStage === undefined &&
455
+ payload.failureCategory === undefined &&
456
+ payload.error === undefined &&
457
+ startupTurnId &&
458
+ sameAttempt &&
459
+ sandbox?.[0].status === "cancelled"
460
+ ) {
461
+ blockedReason = sandbox[0].blockedReason;
462
+ }
463
+ }
464
+ settleStartupPhase(
465
+ setupPhase,
466
+ blockedReason ? "cancelled" : setupStatus,
467
+ numberOrNull(payload.durationMs),
468
+ event.type === "rig.setup.skipped" ? "skipped" : null,
469
+ 0,
470
+ undefined,
471
+ undefined,
472
+ blockedReason,
473
+ );
474
+ continue;
475
+ }
167
476
 
168
477
  switch (event.type) {
169
478
  case "user.message": {
170
479
  // A steering message must not mark in-flight tools complete; it only
171
480
  // ends whatever text was streaming. Turn lifecycle events finalize.
481
+ clearUnscopedTerminalTracking();
172
482
  closeStreamingTail();
173
483
  const childCompletion = workerCompletionPayload(payload.childCompletion);
174
484
  if (childCompletion) {
485
+ workerSessions.add(childCompletion.childSessionId);
486
+ pendingWorkerSessions.delete(childCompletion.childSessionId);
175
487
  items.push({
176
488
  kind: "worker-completion",
177
489
  id: event.id,
@@ -191,6 +503,9 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
191
503
  items.push({
192
504
  kind: "user-message",
193
505
  id: event.id,
506
+ ...(event.clientEventId
507
+ ? { reconciliationKey: `user-message:${event.clientEventId}` }
508
+ : {}),
194
509
  text: voiceMessage?.text ?? stringValue(payload.text),
195
510
  annotations: timelineAnnotations(payload.annotations),
196
511
  annotationSource: {
@@ -209,13 +524,38 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
209
524
  break;
210
525
  }
211
526
 
527
+ case "user.humanInputResponse": {
528
+ // A structured answer resumes the frozen tool call rather than adding a
529
+ // synthetic user.message to model history. Keep the original question
530
+ // and the resolved human choice together as first-class chat history;
531
+ // neither belongs behind the generic activity fold.
532
+ const item = humanInputItemFromResponse(event, payload, humanInputRequests);
533
+ if (!item) break;
534
+ closeStreamingTail();
535
+ items.push(item);
536
+ break;
537
+ }
538
+
212
539
  case "system.update.delivered": {
213
540
  const inputs = machineInputMembers(payload.members);
541
+ for (const input of inputs) {
542
+ if (input.kind === "child_terminal_result") {
543
+ workerSessions.add(input.sourceId);
544
+ pendingWorkerSessions.delete(input.sourceId);
545
+ }
546
+ }
214
547
  if (inputs.length === 0) break;
215
- // Goal continuations already land as `goal.continuation` GoalRows.
216
- // A solo continuation batch would duplicate that landmark + dump the
217
- // model-facing prompt — skip chrome for that case only.
218
- if (inputs.every((member) => member.kind === "goal_continuation")) {
548
+ // Background command receipts are agent-facing inputs, not chat messages.
549
+ // The durable event remains available to the agent and audit history.
550
+ const visibleInputs = inputs.filter(
551
+ (member) => member.kind !== "background_command_result",
552
+ );
553
+ // Goal continuations already have GoalRows. A batch with no other
554
+ // visible update must not interrupt an in-flight streaming message.
555
+ if (
556
+ visibleInputs.length === 0 ||
557
+ visibleInputs.every((member) => member.kind === "goal_continuation")
558
+ ) {
219
559
  break;
220
560
  }
221
561
  closeStreamingTail();
@@ -223,7 +563,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
223
563
  kind: "machine-input-batch",
224
564
  id: event.id,
225
565
  turnId,
226
- members: inputs,
566
+ members: visibleInputs,
227
567
  occurredAt: event.occurredAt,
228
568
  });
229
569
  break;
@@ -234,30 +574,85 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
234
574
  if (!text) {
235
575
  break;
236
576
  }
577
+ const messageId = stringValue(payload.messageId);
578
+ const messageKey = messageId ? JSON.stringify([turnId, messageId]) : null;
579
+ // Responses providers declare the phase when they announce the item, so
580
+ // a streaming progress note is known as commentary before it completes.
581
+ const phase = assistantMessagePhase(payload.phase);
582
+ if (
583
+ (!completeHistoryPrefix && !knownMessagePrefixTurns.has(turnId)) ||
584
+ (messageKey && incompleteMessageKeys.has(messageKey))
585
+ ) {
586
+ if (messageKey) incompleteMessageKeys.add(messageKey);
587
+ break;
588
+ }
589
+ const identified = messageKey ? identifiedMessages.get(messageKey) : undefined;
590
+ if (identified) {
591
+ if (identified.annotationSource?.eventType !== "agent.message.completed") {
592
+ identified.text += text;
593
+ if (phase && !identified.phase) identified.phase = phase;
594
+ rememberAgentResponse(turnId, identified, false);
595
+ }
596
+ break;
597
+ }
237
598
  const open = last();
238
- if (open?.kind === "agent-message" && open.streaming && open.turnId === turnId) {
599
+ if (!messageKey && open?.kind === "tool-call" && open.status === "running") {
600
+ const previous = latestAgentResponseByTurn.get(turnId);
601
+ const previousIndex = previous ? items.indexOf(previous.item) : -1;
602
+ if (
603
+ previous &&
604
+ !previous.completed &&
605
+ previousIndex >= 0 &&
606
+ items
607
+ .slice(previousIndex + 1)
608
+ .every(
609
+ (item) =>
610
+ item.kind === "tool-call" && item.turnId === turnId && item.status === "running",
611
+ )
612
+ ) {
613
+ // Legacy deltas have no provider message identity. Tool creation
614
+ // alone is not a text boundary; completed output still is.
615
+ previous.item.text += text;
616
+ break;
617
+ }
618
+ }
619
+ if (
620
+ !messageKey &&
621
+ open?.kind === "agent-message" &&
622
+ open.streaming &&
623
+ open.turnId === turnId
624
+ ) {
239
625
  open.text += text;
626
+ rememberAgentResponse(turnId, open, false);
240
627
  break;
241
628
  }
242
629
  closeStreamingTail();
243
- items.push({
630
+ const item: AgentMessageItem = {
244
631
  kind: "agent-message",
245
632
  id: event.id,
246
633
  turnId,
247
634
  text,
635
+ ...(phase ? { phase } : {}),
248
636
  streaming: true,
249
637
  occurredAt: event.occurredAt,
250
- });
638
+ };
639
+ items.push(item);
640
+ if (messageKey) identifiedMessages.set(messageKey, item);
641
+ rememberAgentResponse(turnId, item, false);
251
642
  break;
252
643
  }
253
644
 
254
645
  case "agent.message.completed": {
646
+ knownMessagePrefixTurns.add(turnId);
255
647
  const text = stringValue(payload.text);
648
+ const messageId = stringValue(payload.messageId);
649
+ const messageKey = messageId ? JSON.stringify([turnId, messageId]) : null;
650
+ const phase = assistantMessagePhase(payload.phase);
256
651
  // Reconcile the most recent same-turn agent message — even when
257
652
  // activity (tool calls, reasoning) landed after its deltas — so the
258
653
  // completed text never duplicates the streamed one.
259
- let openIndex = -1;
260
- for (let index = items.length - 1; index >= 0; index -= 1) {
654
+ let openIndex = messageKey ? items.indexOf(identifiedMessages.get(messageKey)!) : -1;
655
+ for (let index = messageKey ? -1 : items.length - 1; index >= 0; index -= 1) {
261
656
  const candidate = items[index];
262
657
  if (candidate?.kind === "agent-message" && candidate.turnId === turnId) {
263
658
  openIndex = index;
@@ -269,15 +664,26 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
269
664
  candidate?.kind === "agent-message" ? candidate : undefined;
270
665
  if (
271
666
  open &&
272
- (open.streaming || !open.text || text === open.text || text.startsWith(open.text))
667
+ (messageKey ||
668
+ open.streaming ||
669
+ !open.text ||
670
+ text === open.text ||
671
+ text.startsWith(open.text))
273
672
  ) {
274
- // The completed text is authoritative when it extends what streamed.
275
- if (!open.text || (text && text.startsWith(open.text))) {
673
+ // Identity makes the final text authoritative even if it corrects the
674
+ // draft; legacy receipts still require an extension match.
675
+ if (!open.text || (text && (messageKey || text.startsWith(open.text)))) {
276
676
  open.text = text || open.text;
277
677
  }
678
+ // A phase-less settlement mirror (the worker's final-output receipt)
679
+ // must not erase the provider-declared phase captured by the SDK item.
680
+ if (phase) open.phase = phase;
681
+ // Completion time is what the footer shows ("finished at"); the
682
+ // first-delta stamp stays as the stream's start.
683
+ if (open.startedAt === undefined && open.occurredAt !== event.occurredAt) {
684
+ open.startedAt = open.occurredAt;
685
+ }
278
686
  open.streaming = false;
279
- // Completion time is what the footer shows ("finished at"); keep the
280
- // first-delta stamp only until this event arrives.
281
687
  open.occurredAt = event.occurredAt;
282
688
  open.annotationSource =
283
689
  text && open.text === text
@@ -299,14 +705,16 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
299
705
  items.splice(openIndex, 1);
300
706
  items.push(open);
301
707
  }
708
+ rememberAgentResponse(turnId, open, true);
302
709
  break;
303
710
  }
304
711
  if (text) {
305
- items.push({
712
+ const item: AgentMessageItem = {
306
713
  kind: "agent-message",
307
714
  id: event.id,
308
715
  turnId,
309
716
  text,
717
+ ...(phase ? { phase } : {}),
310
718
  streaming: false,
311
719
  occurredAt: event.occurredAt,
312
720
  annotationSource: {
@@ -317,7 +725,10 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
317
725
  turnId,
318
726
  text,
319
727
  },
320
- });
728
+ };
729
+ items.push(item);
730
+ if (messageKey) identifiedMessages.set(messageKey, item);
731
+ rememberAgentResponse(turnId, item, true);
321
732
  }
322
733
  break;
323
734
  }
@@ -346,9 +757,16 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
346
757
 
347
758
  case "agent.toolCall.created": {
348
759
  const name = typeof payload.name === "string" ? payload.name : "tool";
760
+ const display = parseToolDisplayMetadata(payload.display);
349
761
  const callId = typeof payload.id === "string" ? payload.id : null;
350
762
  const args = payload.arguments ?? null;
351
763
  closeStreamingTail();
764
+ if (callId && humanInputToolCallIds.has(callId)) {
765
+ // The matching session.humanInput.requested / response lifecycle owns
766
+ // the visible chat card. Rendering the raw tool too would hide the
767
+ // decision behind a duplicate generic "Ask" step.
768
+ break;
769
+ }
352
770
  if (
353
771
  toolMatchesLeaf(name, WORKER_SPAWN_TOOL) ||
354
772
  toolMatchesLeaf(name, WORKER_MESSAGE_TOOL)
@@ -381,6 +799,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
381
799
  (item): item is ToolCallItem => item.kind === "tool-call" && item.callId === callId,
382
800
  );
383
801
  if (existing) {
802
+ if (display) existing.display = display;
384
803
  if (args != null) {
385
804
  existing.arguments = args;
386
805
  }
@@ -397,6 +816,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
397
816
  turnId,
398
817
  callId,
399
818
  name,
819
+ ...(display ? { display } : {}),
400
820
  arguments: args,
401
821
  output: undefined,
402
822
  truncation: null,
@@ -415,12 +835,20 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
415
835
  if (!target) {
416
836
  break;
417
837
  }
838
+ resolvePresentationWait(event, turnId ?? target.turnId);
418
839
  if (target.kind === "worker") {
419
840
  // A worker spawn/message that returns an error flag (or an MCP
420
841
  // isError result) settles to "failed" too, so WorkerRow surfaces it.
421
842
  target.status = isErrorOutput(payload) ? "failed" : "complete";
422
843
  target.workerSessionId = target.workerSessionId ?? extractSessionRef(payload.output);
423
844
  target.failure = target.status === "failed" ? workerFailure(payload.output) : null;
845
+ // A spawned or re-messaged worker owes this session a result.
846
+ if (target.status === "complete" && target.workerSessionId) {
847
+ if (target.action === "spawn") workerSessions.add(target.workerSessionId);
848
+ if (workerSessions.has(target.workerSessionId)) {
849
+ pendingWorkerSessions.add(target.workerSessionId);
850
+ }
851
+ }
424
852
  break;
425
853
  }
426
854
  // An output carrying an explicit error flag (or an MCP isError result)
@@ -452,15 +880,73 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
452
880
  break;
453
881
  }
454
882
 
883
+ case "turn.queued": {
884
+ const queuedTurnId = turnId ?? (typeof payload.turnId === "string" ? payload.turnId : null);
885
+ if (queuedTurnId) {
886
+ queuedAtByTurn.set(queuedTurnId, event.occurredAt);
887
+ }
888
+ break;
889
+ }
890
+
891
+ case "turn.started": {
892
+ if (turnId) clearUnscopedTerminalTracking();
893
+ if (!turnId) break;
894
+ const queuedAt = queuedAtByTurn.get(turnId);
895
+ if (!queuedAt) break;
896
+ // Recovery starts the same logical turn again. Queue latency belongs to
897
+ // the first admission only, so consume the queued timestamp after its
898
+ // first projection instead of manufacturing another queue span.
899
+ queuedAtByTurn.delete(turnId);
900
+ closeStreamingTail();
901
+ settleStartupPhase(
902
+ "queue",
903
+ "complete",
904
+ elapsedDurationMs(queuedAt, event.occurredAt),
905
+ null,
906
+ 0,
907
+ queuedAt,
908
+ `${event.id}-queue`,
909
+ );
910
+ break;
911
+ }
912
+
455
913
  case "sandbox.operation.started":
456
914
  case "sandbox.operation.completed":
457
915
  case "sandbox.operation.failed": {
458
916
  const name = typeof payload.name === "string" ? payload.name : "sandbox";
459
917
  const status = event.type.endsWith(".failed")
460
- ? "failed"
918
+ ? payload.expectedTransition === true
919
+ ? "cancelled"
920
+ : "failed"
461
921
  : event.type.endsWith(".completed")
462
922
  ? "complete"
463
923
  : "running";
924
+ const origin =
925
+ payload.origin === "created" ||
926
+ payload.origin === "restored" ||
927
+ payload.origin === "resumed"
928
+ ? payload.origin
929
+ : null;
930
+ const startupPhase = startupPhaseForSandboxOperation(name);
931
+ if (startupPhase) {
932
+ closeStreamingTail();
933
+ settleStartupPhase(
934
+ startupPhase,
935
+ status,
936
+ numberOrNull(payload.durationMs),
937
+ origin,
938
+ 0,
939
+ undefined,
940
+ undefined,
941
+ status === "cancelled"
942
+ ? (startupLifecycleWaitReason(payload) ??
943
+ (payload.failureCode === "rotation_in_progress"
944
+ ? "rotation_in_progress"
945
+ : undefined))
946
+ : undefined,
947
+ );
948
+ break;
949
+ }
464
950
  // Routine per-turn platform plumbing that runs before EVERY turn to
465
951
  // guarantee box contents survive a re-warm — NOT the agent redoing work:
466
952
  // - repository-clone: idempotent clone check + off-manifest token re-seed;
@@ -471,8 +957,13 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
471
957
  // Rendering either every turn reads as churn. Only FAILURES surface, and
472
958
  // they surface loudly — the failed event below creates its own item even
473
959
  // without a started row.
960
+ // - optional-repository-access: the worker's per-turn report of an
961
+ // automatically attached repository that sat the turn out, reported
962
+ // like a skipped optional clone (event payload + log, no row).
474
963
  if (
475
- (name === "repository-clone" || name === "file-resource-download") &&
964
+ (name === "repository-clone" ||
965
+ name === "file-resource-download" ||
966
+ name === "optional-repository-access") &&
476
967
  status !== "failed"
477
968
  ) {
478
969
  break;
@@ -480,13 +971,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
480
971
  const existing = findOpenSandbox(items, name);
481
972
  if (existing && status !== "running") {
482
973
  existing.status = status;
483
- if (
484
- payload.origin === "created" ||
485
- payload.origin === "restored" ||
486
- payload.origin === "resumed"
487
- ) {
488
- existing.origin = payload.origin;
489
- }
974
+ if (origin) existing.origin = origin;
490
975
  const message = failureMessage(payload);
491
976
  if (message) {
492
977
  existing.output = existing.output ? `${existing.output}\n${message}` : message;
@@ -502,12 +987,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
502
987
  name,
503
988
  command: typeof payload.command === "string" ? payload.command : null,
504
989
  output: failureMessage(payload) ?? "",
505
- origin:
506
- payload.origin === "created" ||
507
- payload.origin === "restored" ||
508
- payload.origin === "resumed"
509
- ? payload.origin
510
- : null,
990
+ origin,
511
991
  status,
512
992
  occurredAt: event.occurredAt,
513
993
  });
@@ -515,6 +995,29 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
515
995
  break;
516
996
  }
517
997
 
998
+ case "agent.model.request": {
999
+ const requestPhase = typeof payload.phase === "string" ? payload.phase : null;
1000
+ const status =
1001
+ requestPhase === "started"
1002
+ ? "running"
1003
+ : requestPhase === "first_byte" || requestPhase === "first_event"
1004
+ ? "complete"
1005
+ : requestPhase === "failed" || requestPhase === "timed_out"
1006
+ ? "failed"
1007
+ : null;
1008
+ if (status) {
1009
+ if (status === "running") closeStreamingTail();
1010
+ settleStartupPhase(
1011
+ "provider_first_byte",
1012
+ status,
1013
+ status === "running" ? null : numberOrNull(payload.durationMs),
1014
+ null,
1015
+ status === "running" ? 2 : 1,
1016
+ );
1017
+ }
1018
+ break;
1019
+ }
1020
+
518
1021
  case "sandbox.command.output.delta": {
519
1022
  // `chunk` is the canonical wire field; text/output are legacy shapes.
520
1023
  const text =
@@ -548,6 +1051,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
548
1051
  if (!isSessionStatus(status)) {
549
1052
  break;
550
1053
  }
1054
+ if (status === "running") resolvePresentationWait(event, turnId);
551
1055
  // Only attention-worthy statuses earn a timeline divider. queued /
552
1056
  // running / idle are machinery telemetry: the header pill carries the
553
1057
  // live status, the shimmer says "running", and the turn chip's duration
@@ -559,30 +1063,57 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
559
1063
  const previous = [...items]
560
1064
  .reverse()
561
1065
  .find((item): item is SessionStatusItem => item.kind === "session-status");
562
- if (previous?.status === status) {
1066
+ if (previous?.status === status && !previous.resolvedAt) {
563
1067
  break;
564
1068
  }
565
- items.push({
1069
+ const item: SessionStatusItem = {
566
1070
  kind: "session-status",
567
1071
  id: event.id,
568
1072
  status,
569
1073
  occurredAt: event.occurredAt,
570
- });
1074
+ };
1075
+ if (status === "requires_action") rememberPresentationWait(item, turnId);
1076
+ else {
1077
+ items.push(item);
1078
+ if (status === "failed") {
1079
+ const key = turnId ?? presentationTurnId;
1080
+ const failures = presentationFailures.get(key) ?? [];
1081
+ failures.push(item);
1082
+ presentationFailures.set(key, failures);
1083
+ }
1084
+ }
571
1085
  break;
572
1086
  }
573
1087
 
574
- case "session.requiresAction": {
575
- finalizeOpen(turnId);
1088
+ case "codex.capacity.waiting": {
576
1089
  items.push({
577
1090
  kind: "notice",
578
1091
  id: event.id,
579
1092
  tone: "waiting",
580
- text: "Approval needed — the turn is paused until someone decides.",
1093
+ text:
1094
+ stringValue(payload.detail) ??
1095
+ stringValue(payload.error) ??
1096
+ "Waiting for Codex capacity.",
581
1097
  occurredAt: event.occurredAt,
582
1098
  });
583
1099
  break;
584
1100
  }
585
1101
 
1102
+ case "session.requiresAction": {
1103
+ finalizeOpen(turnId, "complete", event.occurredAt);
1104
+ rememberPresentationWait(
1105
+ {
1106
+ kind: "notice",
1107
+ id: event.id,
1108
+ tone: "waiting",
1109
+ text: "Approval needed — the turn is paused until someone decides.",
1110
+ occurredAt: event.occurredAt,
1111
+ },
1112
+ turnId,
1113
+ );
1114
+ break;
1115
+ }
1116
+
586
1117
  case "session.context.compaction.requested":
587
1118
  case "session.context.compaction.started": {
588
1119
  closeStreamingTail();
@@ -594,6 +1125,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
594
1125
  estimatedTokensBefore: numberOrNull(payload.estimatedTokensBefore),
595
1126
  estimatedTokensAfter: null,
596
1127
  skipReason: null,
1128
+ providerRejection: null,
597
1129
  implementation:
598
1130
  typeof payload.implementation === "string" ? payload.implementation : null,
599
1131
  occurredAt: event.occurredAt,
@@ -611,6 +1143,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
611
1143
  estimatedTokensBefore: numberOrNull(payload.estimatedTokensBefore),
612
1144
  estimatedTokensAfter: numberOrNull(payload.estimatedTokensAfter),
613
1145
  skipReason: null,
1146
+ providerRejection: null,
614
1147
  implementation:
615
1148
  typeof payload.implementation === "string" ? payload.implementation : null,
616
1149
  occurredAt: event.occurredAt,
@@ -628,6 +1161,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
628
1161
  estimatedTokensBefore: numberOrNull(payload.estimatedTokensBefore),
629
1162
  estimatedTokensAfter: null,
630
1163
  skipReason: typeof payload.reason === "string" ? payload.reason : null,
1164
+ providerRejection: compactionProviderRejection(payload),
631
1165
  implementation:
632
1166
  typeof payload.implementation === "string" ? payload.implementation : null,
633
1167
  occurredAt: event.occurredAt,
@@ -639,6 +1173,29 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
639
1173
  // Recovery requests are durable control-plane evidence, not a user
640
1174
  // message or proof that recovery succeeded. Live state already exposes
641
1175
  // a genuinely recovering session; keep the raw event in Debug/audit.
1176
+ if (turnId) {
1177
+ startupRecoveryRevisionByTurn.set(
1178
+ turnId,
1179
+ (startupRecoveryRevisionByTurn.get(turnId) ?? 0) + 1,
1180
+ );
1181
+ if (
1182
+ !event.duplicateOfEventId &&
1183
+ (!event.turnAssociation || event.turnAssociation === "current")
1184
+ ) {
1185
+ // Human Retry reopens the existing logical turn, unlike a new
1186
+ // prompt. Keep failure evidence, but do not let its old settlement
1187
+ // close the recovered turn's work surface.
1188
+ for (const item of items) {
1189
+ if (item.kind === "turn-end" && item.turnId === turnId && item.outcome === "failed") {
1190
+ item.resumedAt ??= event.occurredAt;
1191
+ }
1192
+ }
1193
+ for (const item of presentationFailures.get(turnId) ?? []) {
1194
+ item.resolvedAt = event.occurredAt;
1195
+ }
1196
+ presentationFailures.delete(turnId);
1197
+ }
1198
+ }
642
1199
  break;
643
1200
  }
644
1201
 
@@ -652,6 +1209,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
652
1209
  case "tool.auth_needed":
653
1210
  case "credential.auth_needed": {
654
1211
  const capability = capabilityAuthorizationRequest(payload.capability);
1212
+ const setupRequest = customMcpSetupRequest(payload.setupRequest);
655
1213
  if (
656
1214
  event.type === "tool.auth_needed" &&
657
1215
  !stringValue(payload.toolName) &&
@@ -674,25 +1232,44 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
674
1232
  id: event.id,
675
1233
  turnId,
676
1234
  serverId: typeof payload.serverId === "string" ? payload.serverId : null,
677
- source: capability
678
- ? "capability"
679
- : event.type === "tool.auth_needed"
680
- ? "tool"
681
- : "credential",
1235
+ canonicalServerId:
1236
+ typeof payload.canonicalServerId === "string" ? payload.canonicalServerId : null,
1237
+ connectionSubjectScope:
1238
+ payload.connectionSubjectScope === "subject" ||
1239
+ payload.connectionSubjectScope === "workspace"
1240
+ ? payload.connectionSubjectScope
1241
+ : null,
1242
+ source:
1243
+ capability || setupRequest
1244
+ ? "capability"
1245
+ : event.type === "tool.auth_needed"
1246
+ ? "tool"
1247
+ : "credential",
682
1248
  providerDomain: stringValue(payload.providerDomain),
683
1249
  connectionId: typeof payload.connectionId === "string" ? payload.connectionId : null,
684
- reason: authNeededReason(payload.reason),
1250
+ authoritySource: payload.authoritySource === "host" ? "host" : null,
1251
+ reason: authNeededReason(
1252
+ payload.authoritySource === "host" && typeof payload.authorizationUrl === "string"
1253
+ ? (payload.hostReason ?? payload.reason)
1254
+ : payload.reason,
1255
+ ),
685
1256
  scopes: stringList(payload.scopes),
686
1257
  resource: typeof payload.resource === "string" ? payload.resource : null,
687
1258
  toolName: typeof payload.toolName === "string" ? payload.toolName : null,
688
1259
  authorizationUrl:
689
1260
  typeof payload.authorizationUrl === "string" ? payload.authorizationUrl : null,
690
1261
  capability,
1262
+ setupRequest,
691
1263
  occurredAt: event.occurredAt,
692
1264
  });
693
1265
  break;
694
1266
  }
695
1267
 
1268
+ case "usage.exhausted": {
1269
+ allowanceNotice(event, turnId);
1270
+ break;
1271
+ }
1272
+
696
1273
  case "turn.completed": {
697
1274
  // A standalone manual compaction uses the turn ledger for fencing and
698
1275
  // recovery, but it is maintenance rather than a conversational turn.
@@ -700,7 +1277,9 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
700
1277
  // adding a generic turn chip would falsely make it look like an
701
1278
  // extra agent response.
702
1279
  if (payload.maintenance === "context_compaction") {
703
- finalizeOpen(turnId);
1280
+ finalizeOpen(turnId, "complete", event.occurredAt);
1281
+ takeAgentResponse(turnId);
1282
+ takePendingWaitOutcome(turnId);
704
1283
  break;
705
1284
  }
706
1285
  // Credit exhaustion arrives as a NOMINALLY completed turn (`detail:
@@ -709,8 +1288,19 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
709
1288
  // Rendering it as a clean "complete" turn is a lie that leaves the
710
1289
  // session looking healthy while every future turn silently dies, so it
711
1290
  // projects exactly like a failed turn plus an explicit notice.
1291
+ const allowance = parseAllowanceExhaustedRefusal(payload);
1292
+ if (allowance) {
1293
+ finalizeOpen(turnId, "complete", event.occurredAt);
1294
+ takeAgentResponse(turnId);
1295
+ takePendingWaitOutcome(turnId);
1296
+ items.push(turnEndItem(event, "failed", ALLOWANCE_TURN_END_TEXT));
1297
+ allowanceNotice(event, turnId);
1298
+ break;
1299
+ }
712
1300
  if (isCreditExhaustionPayload(payload)) {
713
- finalizeOpen(turnId);
1301
+ finalizeOpen(turnId, "complete", event.occurredAt);
1302
+ takeAgentResponse(turnId);
1303
+ takePendingWaitOutcome(turnId);
714
1304
  items.push(turnEndItem(event, "failed", CREDIT_EXHAUSTION_MESSAGE));
715
1305
  items.push({
716
1306
  kind: "notice",
@@ -721,26 +1311,116 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
721
1311
  });
722
1312
  break;
723
1313
  }
724
- finalizeOpen(turnId);
725
- items.push(turnEndItem(event, "complete", null));
1314
+ const latestAgentResponse = takeAgentResponse(turnId);
1315
+ const finalOutput = stringValue(payload.output);
1316
+ const visibleFinalOutput = stripOpaqueCitationTokens(finalOutput).trim();
1317
+ const visibleTrackedResponse = stripOpaqueCitationTokens(
1318
+ latestAgentResponse?.item.text ?? "",
1319
+ ).trim();
1320
+ const finalOutputMirrorsCommentary =
1321
+ latestAgentResponse?.item.phase === "commentary" &&
1322
+ visibleTrackedResponse === visibleFinalOutput;
1323
+ const hasAuthoritativeFinalOutput =
1324
+ Boolean(visibleFinalOutput) && !finalOutputMirrorsCommentary;
1325
+ const pendingWaitOutcome = takePendingWaitOutcome(turnId);
1326
+ if (hasAuthoritativeFinalOutput) {
1327
+ // The final message normally lands as its own completion before
1328
+ // `turn.completed` (older workers commit a copy with it), but legacy or
1329
+ // partially compacted ledgers may retain only the terminal output
1330
+ // receipt. Keep that authoritative response visible instead of
1331
+ // leaving it trapped in raw audit data.
1332
+ if (
1333
+ latestAgentResponse &&
1334
+ !latestAgentResponse.completed &&
1335
+ finalOutput.startsWith(latestAgentResponse.item.text)
1336
+ ) {
1337
+ latestAgentResponse.item.text = finalOutput;
1338
+ latestAgentResponse.item.streaming = false;
1339
+ if (
1340
+ latestAgentResponse.item.startedAt === undefined &&
1341
+ latestAgentResponse.item.occurredAt !== event.occurredAt
1342
+ ) {
1343
+ latestAgentResponse.item.startedAt = latestAgentResponse.item.occurredAt;
1344
+ }
1345
+ latestAgentResponse.item.occurredAt = event.occurredAt;
1346
+ const responseIndex = items.indexOf(latestAgentResponse.item);
1347
+ if (responseIndex >= 0 && responseIndex < items.length - 1) {
1348
+ items.splice(responseIndex, 1);
1349
+ items.push(latestAgentResponse.item);
1350
+ }
1351
+ } else if (visibleTrackedResponse !== visibleFinalOutput) {
1352
+ items.push({
1353
+ kind: "agent-message",
1354
+ id: `${event.id}-output-message`,
1355
+ turnId,
1356
+ text: finalOutput,
1357
+ streaming: false,
1358
+ occurredAt: event.occurredAt,
1359
+ });
1360
+ }
1361
+ }
1362
+ finalizeOpen(turnId, "complete", event.occurredAt);
1363
+ const completedTurn = turnEndItem(event, "complete", null);
1364
+ items.push(completedTurn);
1365
+ if (turnCompletedWithEmptyFinalReply(payload)) {
1366
+ items.push({
1367
+ kind: "notice",
1368
+ id: `${event.id}-empty-final-reply`,
1369
+ tone: "input",
1370
+ text: EMPTY_FINAL_REPLY_NOTICE,
1371
+ recordedOutcome: true,
1372
+ occurredAt: event.occurredAt,
1373
+ });
1374
+ }
1375
+ const hasCompletedFinalResponse =
1376
+ latestAgentResponse?.completed === true &&
1377
+ latestAgentResponse.item.phase !== "commentary" &&
1378
+ Boolean(visibleTrackedResponse);
1379
+ if (!hasAuthoritativeFinalOutput && pendingWaitOutcome) {
1380
+ // Preserve existing prose, including delta-only streams, when a
1381
+ // trailing wait yields without a terminal output receipt.
1382
+ completedTurn.preserveWaitResponse = true;
1383
+ }
1384
+ if (!hasAuthoritativeFinalOutput && !hasCompletedFinalResponse && pendingWaitOutcome) {
1385
+ const notice: NoticeItem = {
1386
+ kind: "notice",
1387
+ id: `${pendingWaitOutcome.id}-visible-outcome`,
1388
+ tone: "waiting",
1389
+ text: waitingOutcomeText(pendingWaitOutcome.reason),
1390
+ recordedOutcome: true,
1391
+ ...(pendingWorkerSessions.size > 0
1392
+ ? { waitingAgents: pendingWorkerSessions.size }
1393
+ : {}),
1394
+ occurredAt: pendingWaitOutcome.occurredAt,
1395
+ };
1396
+ items.push(notice);
1397
+ openWaitNotice = notice;
1398
+ }
726
1399
  break;
727
1400
  }
728
1401
 
729
1402
  case "turn.failed": {
1403
+ takeAgentResponse(turnId);
1404
+ takePendingWaitOutcome(turnId);
730
1405
  const hadActivity = hasTurnActivity(items, turnId);
731
1406
  // Credit death can hide behind fields `failureMessage` doesn't read
732
1407
  // (detail/segmentLimit), so classify the whole payload before falling
733
1408
  // back to the generic error/message extraction.
734
- const failureText = isCreditExhaustionPayload(payload)
735
- ? CREDIT_EXHAUSTION_MESSAGE
736
- : failureMessage(payload);
1409
+ const allowance = parseAllowanceExhaustedRefusal(payload);
1410
+ const failureText = allowance
1411
+ ? ALLOWANCE_TURN_END_TEXT
1412
+ : isCreditExhaustionPayload(payload)
1413
+ ? CREDIT_EXHAUSTION_MESSAGE
1414
+ : failureMessage(payload);
737
1415
  // The TURN failed — the in-flight items did not. Chip doctrine: red is
738
1416
  // spent once, on the turn-level outcome. Items caught mid-flight read
739
1417
  // as calm "interrupted" (same as turn.cancelled); an item that itself
740
1418
  // failed keeps its own failed status from its output event.
741
- finalizeOpen(turnId, "cancelled");
1419
+ finalizeOpen(turnId, "cancelled", event.occurredAt);
742
1420
  items.push(turnEndItem(event, "failed", failureText));
743
- if (!hadActivity) {
1421
+ if (allowance) {
1422
+ allowanceNotice(event, turnId);
1423
+ } else if (!hadActivity) {
744
1424
  items.push({
745
1425
  kind: "notice",
746
1426
  id: event.id,
@@ -752,15 +1432,18 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
752
1432
  break;
753
1433
  }
754
1434
 
1435
+ case "turn.superseded":
755
1436
  case "turn.cancelled": {
756
1437
  // A retraction of a never-started queued turn is not a turn ending —
757
1438
  // the message was withdrawn before any work happened; show nothing.
758
1439
  // A null turnId proves nothing, so it keeps the legacy finalize path.
759
- if (turnId && !prescan.startedTurnIds.has(turnId)) {
1440
+ if (event.type === "turn.cancelled" && turnId && !prescan.startedTurnIds.has(turnId)) {
760
1441
  break;
761
1442
  }
1443
+ takeAgentResponse(turnId);
1444
+ takePendingWaitOutcome(turnId);
762
1445
  const hadActivity = hasTurnActivity(items, turnId);
763
- finalizeOpen(turnId, "cancelled");
1446
+ finalizeOpen(turnId, "cancelled", event.occurredAt);
764
1447
  items.push(turnEndItem(event, "cancelled", null));
765
1448
  if (!hadActivity) {
766
1449
  items.push({
@@ -774,6 +1457,45 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
774
1457
  break;
775
1458
  }
776
1459
 
1460
+ case "instruction.confirmation.recovered": {
1461
+ const receipt = asRecord(payload.receipt);
1462
+ if (receipt.outcome !== "published") break;
1463
+ closeStreamingTail();
1464
+ items.push({
1465
+ kind: "notice",
1466
+ id: event.id,
1467
+ tone: "input",
1468
+ text: "Your previously approved workspace instruction was saved.",
1469
+ occurredAt: event.occurredAt,
1470
+ });
1471
+ break;
1472
+ }
1473
+ case "knowledge.confirmation.recovered":
1474
+ case "knowledge.source.prepared":
1475
+ case "knowledge.source.failed": {
1476
+ const receipt = asRecord(payload.receipt);
1477
+ const outcome = event.type === "knowledge.source.failed" ? "failed" : receipt.outcome;
1478
+ if (
1479
+ (event.type !== "knowledge.confirmation.recovered" &&
1480
+ typeof payload.fileId !== "string") ||
1481
+ !["published", "pending", "rejected", "archived", "failed"].includes(String(outcome))
1482
+ )
1483
+ break;
1484
+ closeStreamingTail();
1485
+ items.push({
1486
+ kind: "knowledge",
1487
+ id: event.id,
1488
+ turnId,
1489
+ fileId: typeof payload.fileId === "string" ? payload.fileId : undefined,
1490
+ filename: typeof payload.filename === "string" ? payload.filename : undefined,
1491
+ entryId: typeof receipt.entryId === "string" ? receipt.entryId : undefined,
1492
+ status: outcome === "failed" ? "failed" : "complete",
1493
+ outcome: outcome as "published" | "pending" | "rejected" | "archived" | "failed",
1494
+ occurredAt: event.occurredAt,
1495
+ });
1496
+ break;
1497
+ }
1498
+
777
1499
  case "memory.saved":
778
1500
  case "memory.corrected": {
779
1501
  // A first-party memory write is a discrete step, not streamed text, so it
@@ -802,7 +1524,11 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
802
1524
  case "goal.paused":
803
1525
  case "goal.resumed":
804
1526
  case "goal.cleared":
1527
+ case "goal.held":
805
1528
  case "goal.continuation": {
1529
+ if (event.type === "goal.held" && payload.actor === "agent") {
1530
+ rememberPendingWaitOutcome(pendingWaitOutcomeByTurn, event, payload, turnId);
1531
+ }
806
1532
  // Agent tool mutations already appear as tool-call rows in the activity
807
1533
  // cluster. Re-emitting them as GoalRow landmarks splits "N steps" mid-turn.
808
1534
  if (shouldSuppressAgentGoalLandmark(event.type, payload)) {
@@ -812,18 +1538,37 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
812
1538
  kind: "goal",
813
1539
  id: event.id,
814
1540
  action: event.type.slice("goal.".length) as GoalItem["action"],
815
- text: goalText(payload),
1541
+ text:
1542
+ event.type === "goal.held" && typeof payload.reason === "string" && payload.reason
1543
+ ? payload.reason
1544
+ : goalText(payload),
816
1545
  occurredAt: event.occurredAt,
817
1546
  });
818
1547
  break;
819
1548
  }
820
1549
 
1550
+ case "session.wait.started": {
1551
+ if (payload.actor === "agent") {
1552
+ rememberPendingWaitOutcome(pendingWaitOutcomeByTurn, event, payload, turnId);
1553
+ }
1554
+ break;
1555
+ }
1556
+
821
1557
  default:
822
1558
  break;
823
1559
  }
824
1560
  }
825
1561
 
1562
+ const sourceEventsById = new Map(events.map((event) => [event.id, event]));
826
1563
  for (const item of items) {
1564
+ const source = sourceEventsById.get(item.id);
1565
+ const canonical = "annotationSource" in item ? item.annotationSource : undefined;
1566
+ item.sourceEvents = [
1567
+ ...(source ? [{ eventId: source.id, sequence: source.sequence }] : []),
1568
+ ...(canonical && canonical.eventId !== source?.id
1569
+ ? [{ eventId: canonical.eventId, sequence: canonical.sequence }]
1570
+ : []),
1571
+ ];
827
1572
  if (item.kind === "agent-message") {
828
1573
  item.text = stripOpaqueCitationTokens(item.text);
829
1574
  }
@@ -849,6 +1594,134 @@ function realtimeVoiceMessage(payload: Record<string, unknown>): {
849
1594
  return null;
850
1595
  }
851
1596
 
1597
+ function humanInputRequestsById(events: SessionEvent[]): Map<string, Record<string, unknown>> {
1598
+ const requests = new Map<string, Record<string, unknown>>();
1599
+ for (const event of events) {
1600
+ if (event.type !== "session.humanInput.requested") continue;
1601
+ const request = asRecord(asRecord(event.payload).request);
1602
+ const requestId = stringValue(request.id);
1603
+ if (requestId) requests.set(requestId, request);
1604
+ }
1605
+ return requests;
1606
+ }
1607
+
1608
+ function humanInputItemFromResponse(
1609
+ event: SessionEvent,
1610
+ payload: Record<string, unknown>,
1611
+ requests: Map<string, Record<string, unknown>>,
1612
+ ): HumanInputItem | null {
1613
+ const response = asRecord(payload.response);
1614
+ const outcome = response.outcome;
1615
+ if (
1616
+ outcome !== "answered" &&
1617
+ outcome !== "skipped" &&
1618
+ outcome !== "expired" &&
1619
+ outcome !== "cancelled"
1620
+ ) {
1621
+ return null;
1622
+ }
1623
+
1624
+ const requestId = stringValue(payload.requestId);
1625
+ const request = requests.get(requestId);
1626
+ const questions = humanInputQuestions(request?.questions);
1627
+ const normalizedResponse: HumanInputResponse =
1628
+ outcome === "answered"
1629
+ ? {
1630
+ outcome,
1631
+ answers: Array.isArray(response.answers)
1632
+ ? response.answers
1633
+ .map(asRecord)
1634
+ .map(humanInputAnswer)
1635
+ .filter((answer): answer is HumanInputAnswer => answer !== null)
1636
+ : [],
1637
+ }
1638
+ : { outcome };
1639
+ const questionsById = new Map(questions.map((question) => [question.id, question]));
1640
+ const answers =
1641
+ normalizedResponse.outcome === "answered"
1642
+ ? normalizedResponse.answers.map((answer) =>
1643
+ humanInputAnswerSummary(answer, questionsById.get(answer.questionId)),
1644
+ )
1645
+ : [];
1646
+ return {
1647
+ kind: "human-input",
1648
+ id: event.id,
1649
+ turnId: event.turnId ?? null,
1650
+ requestId,
1651
+ questions,
1652
+ response: normalizedResponse,
1653
+ answers,
1654
+ occurredAt: event.occurredAt,
1655
+ };
1656
+ }
1657
+
1658
+ function humanInputQuestions(value: unknown): HumanInputQuestion[] {
1659
+ if (!Array.isArray(value)) return [];
1660
+ return value.map(asRecord).flatMap((question) => {
1661
+ const id = stringValue(question.id);
1662
+ const prompt = stringValue(question.prompt);
1663
+ const kind = question.kind;
1664
+ if (
1665
+ !id ||
1666
+ !prompt ||
1667
+ (kind !== "text" && kind !== "single_select" && kind !== "multi_select")
1668
+ ) {
1669
+ return [];
1670
+ }
1671
+ const options = Array.isArray(question.options)
1672
+ ? question.options.map(asRecord).flatMap((option) => {
1673
+ const optionId = stringValue(option.id);
1674
+ const label = stringValue(option.label);
1675
+ return optionId && label
1676
+ ? [{ id: optionId, label, description: stringValue(option.description) || null }]
1677
+ : [];
1678
+ })
1679
+ : [];
1680
+ return [
1681
+ {
1682
+ id,
1683
+ kind,
1684
+ prompt,
1685
+ label: stringValue(question.label) || null,
1686
+ helpText: stringValue(question.helpText) || null,
1687
+ options,
1688
+ required: question.required === true,
1689
+ allowOther: question.allowOther === true,
1690
+ },
1691
+ ];
1692
+ });
1693
+ }
1694
+
1695
+ function humanInputAnswer(value: Record<string, unknown>): HumanInputAnswer | null {
1696
+ const questionId = stringValue(value.questionId);
1697
+ if (!questionId) return null;
1698
+ return {
1699
+ questionId,
1700
+ values: Array.isArray(value.values)
1701
+ ? value.values.filter((answer): answer is string => typeof answer === "string")
1702
+ : [],
1703
+ ...(typeof value.other === "string" && value.other ? { other: value.other } : {}),
1704
+ };
1705
+ }
1706
+
1707
+ function humanInputAnswerSummary(
1708
+ answer: HumanInputAnswer,
1709
+ question: HumanInputQuestion | undefined,
1710
+ ): HumanInputAnswerSummary {
1711
+ const optionLabels = new Map(question?.options.map((option) => [option.id, option.label]) ?? []);
1712
+ const values = answer.values.map((value) => optionLabels.get(value) || value);
1713
+ if (answer.other) values.push(answer.other);
1714
+ const fallbackLabel = answer.questionId
1715
+ .replace(/[_-]+/g, " ")
1716
+ .replace(/\b\w/g, (character) => character.toUpperCase());
1717
+ return {
1718
+ questionId: answer.questionId,
1719
+ label: question?.label || question?.prompt || fallbackLabel,
1720
+ prompt: question?.prompt || fallbackLabel,
1721
+ values,
1722
+ };
1723
+ }
1724
+
852
1725
  function providerNativeToolStatus(rawValue: unknown): ToolCallItem["status"] {
853
1726
  const raw = asRecord(rawValue);
854
1727
  if (raw.type !== "hosted_tool_call") {
@@ -909,8 +1782,16 @@ export function stripOpaqueCitationTokens(text: string): string {
909
1782
  return text.replace(/\s*cite(?:[^]+)+/gu, "");
910
1783
  }
911
1784
 
1785
+ /**
1786
+ * The turn summary's short outcome for a usage ceiling. The adjacent notice row
1787
+ * carries who can raise it and when it resets (and is host-customizable), so
1788
+ * the summary line stays a neutral fact instead of repeating remedy prose.
1789
+ */
1790
+ const ALLOWANCE_TURN_END_TEXT = "Usage limit reached";
1791
+
912
1792
  /** The turn-end payload shape, as `isCreditExhaustion` wants it. */
913
1793
  function isCreditExhaustionPayload(payload: Record<string, unknown>): boolean {
1794
+ if (payload.code === "allowance_exhausted") return false;
914
1795
  return isCreditExhaustion({
915
1796
  error: typeof payload.error === "string" ? payload.error : null,
916
1797
  detail: typeof payload.detail === "string" ? payload.detail : null,
@@ -961,18 +1842,24 @@ export function sessionStatusFromEvents(events: SessionEvent[]): SessionStatus |
961
1842
  non-user span folds behind a turn group, with activity blocks nested inside.
962
1843
  -------------------------------------------------------------------------- */
963
1844
 
1845
+ /** Work items: every activity kind except assistant commentary. */
1846
+ type WorkActivityItem = Exclude<ActivityItem, AgentMessageItem>;
1847
+
964
1848
  /**
965
1849
  * Whether an item clusters into an activity block. A `switch` (not a stringly-
966
1850
  * typed set) so adding an {@link ActivityItem} kind is a compile-time prompt to
967
- * decide its grouping — and it narrows `item` to `ActivityItem` with no cast.
1851
+ * decide its grouping, and it narrows `item` to a work item with no cast.
1852
+ * Assistant prose stays outside activity clusters.
968
1853
  */
969
- function isActivityItem(item: TimelineItem): item is ActivityItem {
1854
+ function isActivityItem(item: TimelineItem): item is WorkActivityItem {
970
1855
  switch (item.kind) {
971
1856
  case "reasoning":
972
1857
  case "tool-call":
973
1858
  case "worker":
974
1859
  case "sandbox":
1860
+ case "startup-phase":
975
1861
  case "memory":
1862
+ case "knowledge":
976
1863
  case "fleet-decision":
977
1864
  return true;
978
1865
  default:
@@ -980,7 +1867,18 @@ function isActivityItem(item: TimelineItem): item is ActivityItem {
980
1867
  }
981
1868
  }
982
1869
 
983
- export function groupTimeline(items: TimelineItem[]): TimelineGroup[] {
1870
+ export type GroupTimelineOptions = {
1871
+ /** Readable assistant prose with an independent work summary for each turn. */
1872
+ readableTurns?: boolean;
1873
+ /** @deprecated Use readableTurns. Cross-turn exchange folding has been removed. */
1874
+ foldExchanges?: boolean;
1875
+ };
1876
+
1877
+ export function groupTimeline(
1878
+ items: TimelineItem[],
1879
+ options: GroupTimelineOptions = {},
1880
+ ): TimelineGroup[] {
1881
+ if (options.readableTurns || options.foldExchanges) return groupReadableTurns(items);
984
1882
  const groups: TimelineGroup[] = [];
985
1883
  for (const item of items) {
986
1884
  if (isActivityItem(item)) {
@@ -1006,32 +1904,429 @@ export function groupTimeline(items: TimelineItem[]): TimelineGroup[] {
1006
1904
  return groups;
1007
1905
  }
1008
1906
 
1907
+ /** Live prose stays primary; settled progress joins its own turn's work history. */
1908
+ function groupReadableTurns(items: TimelineItem[]): TimelineGroup[] {
1909
+ type Work = Extract<TimelineGroup, { kind: "activity" }>;
1910
+ let groups: TimelineGroup[] = [];
1911
+ const turns = new Map<string, Work>();
1912
+ // Turn identity and settlement exist even when a final-only turn has no
1913
+ // disclosure. A late receipt must not become a new turn or revive its clock.
1914
+ const seenTurns = new Set<string>();
1915
+ const settlements = new Map<
1916
+ string,
1917
+ { endedAt: string; outcome?: Work["outcome"]; failureText?: string }
1918
+ >();
1919
+ const compactionTurns = new Map<ContextCompactionItem, string>();
1920
+ const messages = new Map<string, AgentMessageItem[]>();
1921
+ const inputs = new Map<string, Extract<TimelineItem, { kind: "machine-input-batch" }>>();
1922
+ const itemOrder = new Map(items.map((item, index) => [item.id, index]));
1923
+ let legacyTurn = "start";
1924
+ let currentTurn = legacyTurn;
1925
+ for (const item of items) {
1926
+ if (item.kind === "user-message") legacyTurn = item.id;
1927
+ const key = ("turnId" in item && item.turnId) || legacyTurn;
1928
+ // Legacy work still establishes a boundary using its prompt key.
1929
+ // A human message alone does not: it may be steering the existing turn.
1930
+ if (
1931
+ ("turnId" in item && item.turnId) ||
1932
+ isActivityItem(item) ||
1933
+ (item.kind === "agent-message" && item.text.trim())
1934
+ ) {
1935
+ if (key !== currentTurn && !seenTurns.has(key)) {
1936
+ const previous = turns.get(currentTurn);
1937
+ if (!settlements.has(currentTurn)) {
1938
+ settlements.set(currentTurn, { endedAt: item.occurredAt });
1939
+ }
1940
+ if (previous?.work && !previous.work.endedAt) previous.work.endedAt = item.occurredAt;
1941
+ currentTurn = key;
1942
+ }
1943
+ seenTurns.add(key);
1944
+ }
1945
+ const workForTurn = () => {
1946
+ let group = turns.get(key);
1947
+ const firstMessage = messages.get(key)?.[0];
1948
+ const messageStart = firstMessage?.startedAt ?? firstMessage?.occurredAt;
1949
+ const startedAt =
1950
+ messageStart && Date.parse(messageStart) < Date.parse(item.occurredAt)
1951
+ ? messageStart
1952
+ : item.occurredAt;
1953
+ if (!group) {
1954
+ const settlement = settlements.get(key);
1955
+ group = {
1956
+ kind: "activity",
1957
+ id: `work-${key}`,
1958
+ items: [],
1959
+ work: { startedAt, details: [] },
1960
+ };
1961
+ if (settlement) {
1962
+ group.work!.endedAt = settlement.endedAt;
1963
+ if (settlement.outcome) group.outcome = settlement.outcome;
1964
+ if (settlement.failureText) group.failureText = settlement.failureText;
1965
+ }
1966
+ turns.set(key, group);
1967
+ const firstMessageIndex =
1968
+ settlement && firstMessage
1969
+ ? groups.findIndex((entry) => entry.kind === "item" && entry.item === firstMessage)
1970
+ : -1;
1971
+ if (firstMessageIndex >= 0) groups.splice(firstMessageIndex, 0, group);
1972
+ else groups.push(group);
1973
+ } else if (Date.parse(startedAt) < Date.parse(group.work!.startedAt)) {
1974
+ // Completion-only startup receipts can reveal an earlier phase start
1975
+ // after another activity has already established the work row.
1976
+ group.work!.startedAt = startedAt;
1977
+ }
1978
+ return group;
1979
+ };
1980
+ if (isActivityItem(item)) {
1981
+ const group = workForTurn();
1982
+ group.items.push(item);
1983
+ const previous = group.work!.details.at(-1);
1984
+ const preparing = (entry: ActivityItem) =>
1985
+ entry.kind === "startup-phase" || (entry.kind === "reasoning" && !entry.text.trim());
1986
+ if (
1987
+ preparing(item) &&
1988
+ previous?.kind === "activity" &&
1989
+ previous.items.every(preparing) &&
1990
+ previous.items.at(-1) === items[(itemOrder.get(item.id) ?? 0) - 1]
1991
+ )
1992
+ previous.items.push(item);
1993
+ else
1994
+ group.work!.details.push({ kind: "activity", id: `work-item-${item.id}`, items: [item] });
1995
+ delete group.work!.waiting;
1996
+ } else if (item.kind === "turn-end") {
1997
+ if (!item.resumedAt) {
1998
+ settlements.set(key, {
1999
+ endedAt: item.occurredAt,
2000
+ outcome: item.outcome,
2001
+ ...(item.failureText ? { failureText: item.failureText } : {}),
2002
+ });
2003
+ }
2004
+ const group = turns.get(key);
2005
+ if (group?.work) {
2006
+ if (item.resumedAt) {
2007
+ group.work.details.push({
2008
+ kind: "item",
2009
+ item: {
2010
+ kind: "notice",
2011
+ id: item.id,
2012
+ tone: "failed",
2013
+ text: item.failureText ?? "The turn failed before retrying.",
2014
+ occurredAt: item.occurredAt,
2015
+ },
2016
+ });
2017
+ continue;
2018
+ }
2019
+ group.outcome = item.outcome;
2020
+ if (item.failureText) group.failureText = item.failureText;
2021
+ group.work.endedAt = item.occurredAt;
2022
+ }
2023
+ } else if (item.kind === "context-compaction" && item.phase === "compacted") {
2024
+ // A maintenance checkpoint alone is not conversational work. Resolve its
2025
+ // attachment after real activity has established this turn's work row.
2026
+ compactionTurns.set(item, key);
2027
+ groups.push({ kind: "item", item });
2028
+ } else if (
2029
+ item.kind === "machine-input-batch" &&
2030
+ item.members.every((member) => ROUTINE_MACHINE_INPUT_KINDS.has(member.kind))
2031
+ ) {
2032
+ const previous = inputs.get(key);
2033
+ if (previous) {
2034
+ previous.members.push(
2035
+ ...item.members.filter((member) => !previous.members.some((old) => old.id === member.id)),
2036
+ );
2037
+ previous.sourceEvents = [...(previous.sourceEvents ?? []), ...(item.sourceEvents ?? [])];
2038
+ } else {
2039
+ const batch = { ...item, compact: true, members: [...item.members] };
2040
+ inputs.set(key, batch);
2041
+ groups.push({ kind: "item", item: batch });
2042
+ }
2043
+ } else if (
2044
+ item.kind === "worker-completion" ||
2045
+ (item.kind === "goal" && item.action === "continuation")
2046
+ ) {
2047
+ const group = turns.get(currentTurn);
2048
+ if (group?.work) group.work.details.push({ kind: "item", item });
2049
+ else groups.push({ kind: "item", item });
2050
+ } else {
2051
+ const current = turns.get(currentTurn)?.work;
2052
+ // A live approval wait is carried by the turn header ("Waiting for you")
2053
+ // and decided in the host's approval surface; a second in-timeline banner
2054
+ // only repeats it. Once resolved it returns as the quiet recorded marker.
2055
+ const liveApprovalWait =
2056
+ item.kind === "notice" &&
2057
+ item.tone === "waiting" &&
2058
+ !item.recordedOutcome &&
2059
+ !item.resolvedAt &&
2060
+ item.text.startsWith("Approval needed") &&
2061
+ current !== undefined &&
2062
+ !current.endedAt;
2063
+ // Likewise the live "waiting on you" divider: the open turn's header
2064
+ // already reads "Waiting for you · <elapsed>". It returns, resolved, as
2065
+ // the "work resumed" divider once the session runs again.
2066
+ const liveStatusWait =
2067
+ item.kind === "session-status" &&
2068
+ item.status === "requires_action" &&
2069
+ !item.resolvedAt &&
2070
+ current !== undefined &&
2071
+ !current.endedAt;
2072
+ if (!liveApprovalWait && !liveStatusWait) groups.push({ kind: "item", item });
2073
+ if (current && !current.endedAt) {
2074
+ if (
2075
+ item.kind === "notice" &&
2076
+ item.tone === "waiting" &&
2077
+ !item.recordedOutcome &&
2078
+ !item.resolvedAt
2079
+ ) {
2080
+ current.waiting = {
2081
+ label: item.text.startsWith("Approval needed") ? "Waiting for you" : "Waiting",
2082
+ since: item.occurredAt,
2083
+ };
2084
+ } else if (
2085
+ item.kind === "session-status" &&
2086
+ item.status === "requires_action" &&
2087
+ !item.resolvedAt
2088
+ ) {
2089
+ current.waiting = { label: "Waiting for you", since: item.occurredAt };
2090
+ } else if (
2091
+ item.kind === "session-status" &&
2092
+ ["paused", "cancelled", "failed"].includes(item.status) &&
2093
+ !item.resolvedAt
2094
+ ) {
2095
+ current.endedAt = item.occurredAt;
2096
+ delete current.waiting;
2097
+ }
2098
+ }
2099
+ if (item.kind === "agent-message" && item.text.trim()) {
2100
+ const prose = messages.get(key) ?? [];
2101
+ prose.push(item);
2102
+ messages.set(key, prose);
2103
+ // A text-only turn still has one overall activity surface. An explicit
2104
+ // final-only response does not manufacture an empty work disclosure.
2105
+ if (
2106
+ (item.phase !== "final_answer" || prose.length > 1) &&
2107
+ (turns.has(key) || !settlements.has(key))
2108
+ )
2109
+ workForTurn();
2110
+ }
2111
+ }
2112
+ }
2113
+ groups = groups.filter((entry) => {
2114
+ if (entry.kind !== "item" || entry.item.kind !== "context-compaction") return true;
2115
+ const key = compactionTurns.get(entry.item);
2116
+ const work = key === undefined ? undefined : turns.get(key)?.work;
2117
+ if (!work) return true;
2118
+ work.details.push(entry);
2119
+ return false;
2120
+ });
2121
+ // A declared answer starts the response clock immediately. Without a phase,
2122
+ // only settlement identifies the last response; text length never does.
2123
+ const beforeRows = new Map<TimelineGroup, Work>();
2124
+ const afterRows = new Map<TimelineGroup, Work>();
2125
+ const movedRows = new Set<Work>();
2126
+ const foldedProse = new Set<AgentMessageItem>();
2127
+ const positions = new Map<TimelineGroup | TimelineItem, number>();
2128
+ const nextBoundary = new Map<Work, number>();
2129
+ let boundary = groups.length;
2130
+ for (let index = groups.length - 1; index >= 0; index -= 1) {
2131
+ const entry = groups[index]!;
2132
+ positions.set(entry.kind === "item" ? entry.item : entry, index);
2133
+ if (entry.kind === "activity" && entry.work) {
2134
+ nextBoundary.set(entry, boundary);
2135
+ boundary = index;
2136
+ } else if (
2137
+ entry.kind === "item" &&
2138
+ (entry.item.kind === "user-message" || entry.item.kind === "machine-input-batch")
2139
+ ) {
2140
+ boundary = index;
2141
+ }
2142
+ }
2143
+ for (const [key, group] of turns) {
2144
+ const prose = messages.get(key) ?? [];
2145
+ const settledAt = group.work!.endedAt;
2146
+ const responseCandidates = settledAt
2147
+ ? prose.filter(
2148
+ (message) => Date.parse(message.startedAt ?? message.occurredAt) <= Date.parse(settledAt),
2149
+ )
2150
+ : prose;
2151
+ const response =
2152
+ responseCandidates.filter((message) => message.phase === "final_answer").at(-1) ??
2153
+ (settledAt
2154
+ ? (responseCandidates.at(-1) ??
2155
+ prose.filter((message) => message.phase === "final_answer").at(-1))
2156
+ : undefined);
2157
+ if (settledAt) {
2158
+ for (const message of responseCandidates) {
2159
+ // Markdown image syntax also carries retained video/audio previews.
2160
+ // Keep potential primary media visible rather than guessing whether a
2161
+ // partial/reference-style embed can safely disappear into history.
2162
+ if (message === response || message.text.includes("![")) continue;
2163
+ foldedProse.add(message);
2164
+ group.work!.details.push({ kind: "item", item: message });
2165
+ }
2166
+ }
2167
+ // Preserve event chronology, not completion timestamps or activity kinds.
2168
+ group.work!.details.sort((a, b) => {
2169
+ const first = (entry: TimelineGroup) =>
2170
+ entry.kind === "item"
2171
+ ? entry.item.id
2172
+ : entry.kind === "activity"
2173
+ ? entry.items[0]?.id
2174
+ : undefined;
2175
+ return (itemOrder.get(first(a) ?? "") ?? 0) - (itemOrder.get(first(b) ?? "") ?? 0);
2176
+ });
2177
+ if (response) {
2178
+ const responseAt = response.startedAt ?? response.occurredAt;
2179
+ // A delayed completion-only receipt cannot extend a settled work clock.
2180
+ group.work!.responseStartedAt =
2181
+ settledAt && Date.parse(responseAt) > Date.parse(settledAt) ? settledAt : responseAt;
2182
+ // A late completion may belong to an older turn. Keep its work at its
2183
+ // original boundary rather than moving it across a newer turn's input.
2184
+ if (
2185
+ settledAt &&
2186
+ (positions.get(response) ?? groups.length) < (nextBoundary.get(group) ?? groups.length)
2187
+ ) {
2188
+ beforeRows.set(groups[positions.get(response)!]!, group);
2189
+ movedRows.add(group);
2190
+ }
2191
+ }
2192
+ if (!settledAt) {
2193
+ // Keep the same row at the tail of this turn, after every live progress
2194
+ // message and attention surface, without crossing a newer input/turn.
2195
+ // A visible steer is not a turn boundary when later prose still belongs
2196
+ // to this turn. New input without that evidence remains a hard boundary.
2197
+ const lastProseIndex = Math.max(...prose.map((message) => positions.get(message) ?? -1));
2198
+ let tailIndex = groups.length - 1;
2199
+ for (let index = (positions.get(group) ?? 0) + 1; index <= tailIndex; index += 1) {
2200
+ const entry = groups[index]!;
2201
+ if (
2202
+ (entry.kind === "activity" && entry.work) ||
2203
+ (entry.kind === "item" &&
2204
+ (("turnId" in entry.item && entry.item.turnId && entry.item.turnId !== key) ||
2205
+ (index > lastProseIndex &&
2206
+ (entry.item.kind === "user-message" || entry.item.kind === "machine-input-batch"))))
2207
+ ) {
2208
+ tailIndex = index - 1;
2209
+ break;
2210
+ }
2211
+ }
2212
+ const tail = groups[tailIndex];
2213
+ if (tail && tail !== group) {
2214
+ afterRows.set(tail, group);
2215
+ movedRows.add(group);
2216
+ }
2217
+ }
2218
+ }
2219
+ return groups.flatMap((group) => {
2220
+ if (group.kind === "activity" && movedRows.has(group)) return [];
2221
+ const hidden =
2222
+ group.kind === "item" && group.item.kind === "agent-message" && foldedProse.has(group.item);
2223
+ const before = beforeRows.get(group);
2224
+ const after = afterRows.get(group);
2225
+ return [...(before ? [before] : []), ...(hidden ? [] : [group]), ...(after ? [after] : [])];
2226
+ });
2227
+ }
2228
+
2229
+ /** Machine inputs that continue the current exchange rather than start one. */
2230
+ const ROUTINE_MACHINE_INPUT_KINDS: ReadonlySet<MachineInputMember["kind"]> = new Set([
2231
+ "goal_continuation",
2232
+ "background_command_result",
2233
+ "session_wait_timeout",
2234
+ "agent_message",
2235
+ "agent_steer_instruction",
2236
+ "child_terminal_result",
2237
+ "child_requires_action_resolved",
2238
+ "child_paused",
2239
+ "child_waiting_capacity",
2240
+ "child_progress",
2241
+ ]);
2242
+
1009
2243
  /* --- helpers ---------------------------------------------------------------- */
1010
2244
 
1011
2245
  type TurnAnchorPrescan = {
1012
2246
  queuedTurnByTrigger: Map<string, string>;
1013
2247
  startSeqByTrigger: Map<string, number>;
1014
2248
  cancelledBeforeStartTriggers: Set<string>;
2249
+ directChatSequenceByTrigger: Map<string, number>;
2250
+ explicitQueuedTriggers: Set<string>;
1015
2251
  startedTurnIds: Set<string>;
1016
2252
  };
1017
2253
 
2254
+ /** Navigation uses the same queue/start/withdrawal rules as transcript ordering. */
2255
+ export function timelineQuestionPlacement(question: SessionEvent, evidence: SessionEvent[]) {
2256
+ const anchors = prescanTurnAnchors([question, ...evidence]);
2257
+ const direct = anchors.directChatSequenceByTrigger.get(question.id);
2258
+ if (direct !== undefined) return { kind: "visible" as const, sequence: direct };
2259
+ if (anchors.cancelledBeforeStartTriggers.has(question.id)) return { kind: "withdrawn" as const };
2260
+ const start = anchors.startSeqByTrigger.get(question.id);
2261
+ if (start !== undefined) return { kind: "visible" as const, sequence: start };
2262
+ if (
2263
+ anchors.queuedTurnByTrigger.has(question.id) ||
2264
+ anchors.explicitQueuedTriggers.has(question.id)
2265
+ )
2266
+ return { kind: "pending" as const };
2267
+ return { kind: "visible" as const, sequence: question.sequence };
2268
+ }
2269
+
1018
2270
  function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
1019
2271
  const ordered = [...events].sort((a, b) => a.sequence - b.sequence);
1020
2272
  const queuedTurnByTrigger = new Map<string, string>();
1021
2273
  const startSeqByTrigger = new Map<string, number>();
1022
2274
  const cancelledTurnIds = new Set<string>();
2275
+ const withdrawnTurnIds = new Set<string>();
1023
2276
  const fallbackSeqByTurn = new Map<string, number>();
2277
+ const directChatSequenceByTrigger = new Map<string, number>();
2278
+ const explicitQueuedTriggers = new Set<string>();
2279
+ const queueSteerSequenceByTurn = new Map<string, number>();
1024
2280
  const startedTurnIds = new Set<string>();
2281
+ const userMessageSequenceById = new Map<string, number>();
1025
2282
 
1026
2283
  for (const event of ordered) {
1027
2284
  const payload = asRecord(event.payload);
1028
2285
  const turnId = event.turnId ?? null;
2286
+ if (event.type === "user.message") {
2287
+ userMessageSequenceById.set(event.id, event.sequence);
2288
+ if (payload.routing === "queued_for_execution") {
2289
+ explicitQueuedTriggers.add(event.id);
2290
+ } else if (
2291
+ payload.routing === "accepted_for_execution" ||
2292
+ payload.routing === "accepted_for_steering"
2293
+ ) {
2294
+ directChatSequenceByTrigger.set(event.id, event.sequence);
2295
+ }
2296
+ }
2297
+ if (event.type === "user.message" && payload.delivery === "steer") {
2298
+ directChatSequenceByTrigger.set(event.id, event.sequence);
2299
+ }
2300
+ if (event.type === "session.control.steer_requested") {
2301
+ const targetTurnId = typeof payload.targetTurnId === "string" ? payload.targetTurnId : null;
2302
+ if (targetTurnId && !queueSteerSequenceByTurn.has(targetTurnId)) {
2303
+ queueSteerSequenceByTurn.set(targetTurnId, event.sequence);
2304
+ }
2305
+ }
2306
+ if (
2307
+ event.type === "session.queue.changed" &&
2308
+ (payload.operation === "edit" || payload.operation === "delete")
2309
+ ) {
2310
+ const withdrawnTurnId =
2311
+ typeof payload.turnId === "string" ? payload.turnId : (turnId ?? null);
2312
+ if (withdrawnTurnId) withdrawnTurnIds.add(withdrawnTurnId);
2313
+ }
1029
2314
  if (event.type === "turn.queued") {
1030
2315
  const triggerEventId =
1031
2316
  typeof payload.triggerEventId === "string" ? payload.triggerEventId : null;
1032
2317
  const queuedTurnId = typeof payload.turnId === "string" ? payload.turnId : turnId;
1033
2318
  if (triggerEventId && queuedTurnId) {
1034
2319
  queuedTurnByTrigger.set(triggerEventId, queuedTurnId);
2320
+ if (
2321
+ (payload.routing === "accepted_for_execution" ||
2322
+ payload.routing === "accepted_for_steering") &&
2323
+ !directChatSequenceByTrigger.has(triggerEventId)
2324
+ ) {
2325
+ directChatSequenceByTrigger.set(
2326
+ triggerEventId,
2327
+ userMessageSequenceById.get(triggerEventId) ?? event.sequence,
2328
+ );
2329
+ }
1035
2330
  }
1036
2331
  continue;
1037
2332
  }
@@ -1060,6 +2355,10 @@ function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
1060
2355
  }
1061
2356
 
1062
2357
  for (const [triggerEventId, turnId] of queuedTurnByTrigger) {
2358
+ const queueSteerSequence = queueSteerSequenceByTurn.get(turnId);
2359
+ if (queueSteerSequence !== undefined) {
2360
+ directChatSequenceByTrigger.set(triggerEventId, queueSteerSequence);
2361
+ }
1063
2362
  if (!startSeqByTrigger.has(triggerEventId)) {
1064
2363
  const fallbackSeq = fallbackSeqByTurn.get(turnId);
1065
2364
  if (fallbackSeq !== undefined) {
@@ -1071,7 +2370,7 @@ function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
1071
2370
  const cancelledBeforeStartTriggers = new Set<string>();
1072
2371
  for (const [triggerEventId, turnId] of queuedTurnByTrigger) {
1073
2372
  if (
1074
- cancelledTurnIds.has(turnId) &&
2373
+ (cancelledTurnIds.has(turnId) || withdrawnTurnIds.has(turnId)) &&
1075
2374
  !startSeqByTrigger.has(triggerEventId) &&
1076
2375
  !fallbackSeqByTurn.has(turnId)
1077
2376
  ) {
@@ -1083,6 +2382,8 @@ function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
1083
2382
  queuedTurnByTrigger,
1084
2383
  startSeqByTrigger,
1085
2384
  cancelledBeforeStartTriggers,
2385
+ directChatSequenceByTrigger,
2386
+ explicitQueuedTriggers,
1086
2387
  startedTurnIds,
1087
2388
  };
1088
2389
  }
@@ -1095,8 +2396,22 @@ function orderTimelineEvents(events: SessionEvent[], prescan: TurnAnchorPrescan)
1095
2396
  if (event.type !== "user.message") {
1096
2397
  continue;
1097
2398
  }
2399
+ // Steer is an accepted direction change, not waiting queue copy. Keep it
2400
+ // directly in chat across refresh/reconnect even before its turn starts.
2401
+ const directChatSequence = prescan.directChatSequenceByTrigger.get(event.id);
2402
+ if (directChatSequence !== undefined) {
2403
+ pushInsertion(insertions, directChatSequence, event);
2404
+ continue;
2405
+ }
1098
2406
  const queuedTurnId = prescan.queuedTurnByTrigger.get(event.id);
1099
2407
  if (!queuedTurnId) {
2408
+ if (prescan.explicitQueuedTriggers.has(event.id)) {
2409
+ const startSeq = prescan.startSeqByTrigger.get(event.id);
2410
+ if (startSeq !== undefined) {
2411
+ pushInsertion(insertions, startSeq, event);
2412
+ }
2413
+ continue;
2414
+ }
1100
2415
  pushInsertion(insertions, event.sequence, event);
1101
2416
  continue;
1102
2417
  }
@@ -1150,16 +2465,21 @@ function isAgentActivityEvent(type: string): boolean {
1150
2465
  * steering, or deleting a waiting row must never make its prompt appear in the
1151
2466
  * transcript as though inference had begun.
1152
2467
  */
1153
- function isTurnExecutionEvidence(type: string): boolean {
2468
+ export function isTurnExecutionEvidence(type: string): boolean {
1154
2469
  return (
1155
2470
  isAgentActivityEvent(type) ||
1156
2471
  type === "turn.completed" ||
1157
2472
  type === "turn.failed" ||
2473
+ // An admission refusal can settle a queued turn before it ever starts:
2474
+ // its prompt still belongs above the "usage limit reached" row.
2475
+ type === "usage.exhausted" ||
2476
+ type === "turn.superseded" ||
1158
2477
  type === "turn.recovery.requested" ||
1159
2478
  type === "turn.capacity_waiting" ||
1160
2479
  type === "session.requiresAction" ||
1161
2480
  type === "tool.auth_needed" ||
1162
2481
  type === "credential.auth_needed" ||
2482
+ type.startsWith("turn.startup.phase.") ||
1163
2483
  type.startsWith("rig.setup.") ||
1164
2484
  type === "codex.capacity.waiting" ||
1165
2485
  type === "codex.capacity.resumed" ||
@@ -1263,9 +2583,39 @@ function foldSettledTurn(groups: TimelineGroup[], turnEnd: TurnEndItem): void {
1263
2583
  if (collected.length === 0) {
1264
2584
  return;
1265
2585
  }
1266
-
1267
- const finalMessage = extractFinalAgentMessage(collected, turnEnd);
1268
- const body = finalMessage ? collected.slice(0, -1) : collected;
2586
+ const collectedLength = collected.length;
2587
+
2588
+ // Resolve only inside this turn's collected suffix. A user/machine-input
2589
+ // boundary or an interleaved foreign turn must never donate its answer.
2590
+ const waitMessages = turnEnd.preserveWaitResponse
2591
+ ? collected.filter(
2592
+ (group): group is Extract<TimelineGroup, { kind: "item" }> =>
2593
+ group.kind === "item" &&
2594
+ group.item.kind === "agent-message" &&
2595
+ !group.item.streaming &&
2596
+ group.item.text.trim().length > 0 &&
2597
+ belongsToTurn(group.item, turnEnd.turnId),
2598
+ )
2599
+ : [];
2600
+ // A completed answer outranks later commentary. Otherwise use the latest
2601
+ // visible existing prose, ignoring whitespace and opaque-only stream tails.
2602
+ const waitResponse =
2603
+ waitMessages
2604
+ .slice()
2605
+ .reverse()
2606
+ .find(
2607
+ (group) =>
2608
+ group.item.kind === "agent-message" &&
2609
+ group.item.phase !== "commentary" &&
2610
+ group.item.annotationSource?.eventType === "agent.message.completed",
2611
+ ) ?? waitMessages.at(-1);
2612
+ const finalMessage = waitResponse ?? extractFinalAgentMessage(collected, turnEnd);
2613
+ const fallbackMessage =
2614
+ finalMessage || hasOrdinaryFinalAgentMessage(collected, turnEnd)
2615
+ ? null
2616
+ : extractLatestCompletedCommentary(collected, turnEnd);
2617
+ const visibleMessage = finalMessage ?? fallbackMessage;
2618
+ const body = visibleMessage ? collected.filter((group) => group !== visibleMessage) : collected;
1269
2619
  if (body.length === 0) {
1270
2620
  return;
1271
2621
  }
@@ -1293,8 +2643,8 @@ function foldSettledTurn(groups: TimelineGroup[], turnEnd: TurnEndItem): void {
1293
2643
 
1294
2644
  groups.splice(
1295
2645
  startIndex,
1296
- collected.length,
1297
- ...(finalMessage ? [turnGroup, finalMessage] : [turnGroup]),
2646
+ collectedLength,
2647
+ ...(visibleMessage ? [turnGroup, visibleMessage] : [turnGroup]),
1298
2648
  );
1299
2649
  }
1300
2650
 
@@ -1303,9 +2653,9 @@ function isTurnBoundary(group: TimelineGroup | undefined): boolean {
1303
2653
  group?.kind === "turn" ||
1304
2654
  (group?.kind === "item" &&
1305
2655
  (group.item.kind === "user-message" ||
2656
+ group.item.kind === "human-input" ||
1306
2657
  group.item.kind === "context-compaction" ||
1307
- (group.item.kind === "machine-input-batch" &&
1308
- group.item.members.some((member) => member.kind === "media_generation_result")) ||
2658
+ group.item.kind === "machine-input-batch" ||
1309
2659
  (group.item.kind === "notice" && group.item.tone === "input")))
1310
2660
  );
1311
2661
  }
@@ -1350,6 +2700,43 @@ function extractFinalAgentMessage(
1350
2700
  return tail;
1351
2701
  }
1352
2702
 
2703
+ function hasOrdinaryFinalAgentMessage(groups: TimelineGroup[], turnEnd: TurnEndItem): boolean {
2704
+ return groups.some((group) => {
2705
+ if (group.kind !== "item" || group.item.kind !== "agent-message") return false;
2706
+ const message = group.item;
2707
+ return (
2708
+ !message.streaming &&
2709
+ message.phase !== "commentary" &&
2710
+ message.text.trim().length > 0 &&
2711
+ belongsToTurn(message, turnEnd.turnId)
2712
+ );
2713
+ });
2714
+ }
2715
+
2716
+ function extractLatestCompletedCommentary(
2717
+ groups: TimelineGroup[],
2718
+ turnEnd: TurnEndItem,
2719
+ ): Extract<TimelineGroup, { kind: "item" }> | null {
2720
+ for (let index = groups.length - 1; index >= 0; index -= 1) {
2721
+ const group = groups[index];
2722
+ if (group?.kind !== "item" || group.item.kind !== "agent-message") continue;
2723
+ const message = group.item;
2724
+ if (
2725
+ !message.streaming &&
2726
+ message.phase === "commentary" &&
2727
+ message.text.trim().length > 0 &&
2728
+ belongsToTurn(message, turnEnd.turnId)
2729
+ ) {
2730
+ return group;
2731
+ }
2732
+ }
2733
+ return null;
2734
+ }
2735
+
2736
+ function belongsToTurn(message: AgentMessageItem, turnId: string | null): boolean {
2737
+ return !message.turnId || !turnId || message.turnId === turnId;
2738
+ }
2739
+
1353
2740
  function groupStartedAt(group: TimelineGroup | undefined): string | undefined {
1354
2741
  if (!group) {
1355
2742
  return undefined;
@@ -1372,10 +2759,37 @@ function stringValue(value: unknown): string {
1372
2759
  return typeof value === "string" ? value : "";
1373
2760
  }
1374
2761
 
2762
+ function assistantMessagePhase(value: unknown): AgentMessageItem["phase"] {
2763
+ return value === "commentary" || value === "final_answer" ? value : undefined;
2764
+ }
2765
+
1375
2766
  function numberOrNull(value: unknown): number | null {
1376
2767
  return typeof value === "number" && Number.isFinite(value) ? value : null;
1377
2768
  }
1378
2769
 
2770
+ /**
2771
+ * Read the closed provider-rejection record a `summarization_failed` skip
2772
+ * carries. Only bounded identifiers are accepted; a provider message is never
2773
+ * part of the payload and would be ignored here anyway.
2774
+ */
2775
+ function compactionProviderRejection(
2776
+ payload: Record<string, unknown>,
2777
+ ): ContextCompactionItem["providerRejection"] {
2778
+ const record = asRecord(payload.providerRejection);
2779
+ if (typeof record.httpStatus !== "number" || !Number.isFinite(record.httpStatus)) {
2780
+ return null;
2781
+ }
2782
+ const text = (value: unknown): string | null =>
2783
+ typeof value === "string" && value.trim() ? value : null;
2784
+ return {
2785
+ httpStatus: record.httpStatus,
2786
+ type: text(record.type),
2787
+ code: text(record.code),
2788
+ param: text(record.param),
2789
+ requestId: text(record.requestId),
2790
+ };
2791
+ }
2792
+
1379
2793
  function compactionTrigger(payload: Record<string, unknown>): ContextCompactionItem["trigger"] {
1380
2794
  const trigger = payload.trigger;
1381
2795
  return trigger === "auto" ||
@@ -1431,10 +2845,17 @@ function machineInputMembers(value: unknown): MachineInputBatchItem["members"] {
1431
2845
  const kinds = new Set<MachineInputBatchItem["members"][number]["kind"]>([
1432
2846
  "scheduled_occurrence",
1433
2847
  "goal_continuation",
2848
+ "background_command_result",
2849
+ "session_wait_timeout",
1434
2850
  "agent_message",
1435
2851
  "agent_steer_instruction",
1436
2852
  "child_terminal_result",
1437
2853
  "media_generation_result",
2854
+ "child_requires_action",
2855
+ "child_requires_action_resolved",
2856
+ "child_paused",
2857
+ "child_waiting_capacity",
2858
+ "child_progress",
1438
2859
  ]);
1439
2860
  const classifications = new Set<MachineInputBatchItem["members"][number]["classification"]>([
1440
2861
  "success",
@@ -1481,6 +2902,8 @@ const SESSION_STATUSES: readonly SessionStatus[] = [
1481
2902
  "running",
1482
2903
  "idle",
1483
2904
  "requires_action",
2905
+ "recovering",
2906
+ "waiting_capacity",
1484
2907
  "failed",
1485
2908
  "cancelled",
1486
2909
  ];
@@ -1624,6 +3047,10 @@ function findOpenCall(
1624
3047
  if (byId) {
1625
3048
  return byId as ToolCallItem | WorkerItem;
1626
3049
  }
3050
+ // An explicit provider correlation id is authoritative. Falling back to
3051
+ // an unrelated running call corrupts its status/output when compact pages
3052
+ // or interleaved providers deliver an unmatched historical result.
3053
+ return undefined;
1627
3054
  }
1628
3055
  return reversed.find(
1629
3056
  (item): item is ToolCallItem | WorkerItem => isCall(item) && item.status === "running",
@@ -1639,16 +3066,58 @@ function findOpenSandbox(items: TimelineItem[], name: string): SandboxItem | und
1639
3066
  );
1640
3067
  }
1641
3068
 
1642
- function failureMessage(payload: Record<string, unknown>): string | null {
1643
- for (const key of ["error", "message"] as const) {
1644
- const value = payload[key];
1645
- if (typeof value === "string" && value.trim().length > 0) {
1646
- // Auth/quota provider errors are rewritten for the right audience
1647
- // (raw text remains in the event payload for debug surfaces).
1648
- return humanizeFailureReason(value);
1649
- }
3069
+ function startupPhaseForSandboxOperation(name: string): StartupPhase | null {
3070
+ return SANDBOX_STARTUP_PHASES[name] ?? null;
3071
+ }
3072
+
3073
+ function startupPhaseFromPayload(value: unknown): StartupPhase | null {
3074
+ return typeof value === "string" && STARTUP_PHASES.has(value as StartupPhase)
3075
+ ? (value as StartupPhase)
3076
+ : null;
3077
+ }
3078
+
3079
+ function startupLifecycleWaitReason(
3080
+ payload: Record<string, unknown>,
3081
+ ): StartupPhaseItem["blockedReason"] {
3082
+ if (
3083
+ payload.expectedTransition !== true ||
3084
+ payload.failureStage !== "lifecycle_wait" ||
3085
+ payload.failureCategory !== "drain_capture_wait"
3086
+ ) {
3087
+ return undefined;
1650
3088
  }
1651
- return null;
3089
+ const code = payload.failureCode;
3090
+ return code === "capture_in_progress" ||
3091
+ code === "rotation_in_progress" ||
3092
+ code === "provider_recovery_in_progress"
3093
+ ? code
3094
+ : undefined;
3095
+ }
3096
+
3097
+ const SANDBOX_STARTUP_PHASES: Record<string, StartupPhase> = {
3098
+ "sandbox.provision": "sandbox",
3099
+ "repository-clone": "repository",
3100
+ "file-resource-download": "files",
3101
+ };
3102
+ const STARTUP_PHASES = new Set<StartupPhase>([
3103
+ "queue",
3104
+ "sandbox",
3105
+ "rig",
3106
+ "repository",
3107
+ "files",
3108
+ "tools",
3109
+ "model_preparation",
3110
+ "provider_first_byte",
3111
+ ]);
3112
+
3113
+ function elapsedDurationMs(startedAt: string, completedAt: string): number | null {
3114
+ const start = Date.parse(startedAt);
3115
+ const end = Date.parse(completedAt);
3116
+ return Number.isFinite(start) && Number.isFinite(end) ? Math.max(0, end - start) : null;
3117
+ }
3118
+
3119
+ function failureMessage(payload: Record<string, unknown>): string | null {
3120
+ return presentFailure(payload).reason;
1652
3121
  }
1653
3122
 
1654
3123
  function goalText(payload: Record<string, unknown>): string | null {
@@ -1665,17 +3134,41 @@ function goalText(payload: Record<string, unknown>): string | null {
1665
3134
  return null;
1666
3135
  }
1667
3136
 
3137
+ function rememberPendingWaitOutcome(
3138
+ pending: Map<string | null, PendingWaitOutcome>,
3139
+ event: SessionEvent,
3140
+ payload: Record<string, unknown>,
3141
+ eventTurnId: string | null,
3142
+ ): void {
3143
+ const reason = stringValue(payload.reason).trim();
3144
+ if (!reason) return;
3145
+ const turnId =
3146
+ eventTurnId || stringValue(payload.waitTurnId) || stringValue(payload.turnId) || null;
3147
+ pending.set(turnId, { id: event.id, reason, occurredAt: event.occurredAt });
3148
+ }
3149
+
3150
+ function waitingOutcomeText(reason: string): string {
3151
+ return /^waiting\b/i.test(reason) ? reason : `Waiting: ${reason}`;
3152
+ }
3153
+
1668
3154
  /**
1669
3155
  * Agent-owned goal mutations already have an in-cluster tool row. Suppress the
1670
- * breakaway landmark for those only. `goal.completed` has no actor field today
1671
- * and is only emitted by the agent tool, so it is always suppressed. API /
1672
- * system / create-session / continuation landmarks stay visible.
3156
+ * breakaway landmark for those only. Empty wait turns retain the durable hold
3157
+ * reason and surface it after the folded steps. `goal.completed` has no actor
3158
+ * field today and is only emitted by the agent tool, so it is always
3159
+ * suppressed. API / system / create-session / continuation landmarks stay
3160
+ * visible.
1673
3161
  */
1674
3162
  function shouldSuppressAgentGoalLandmark(type: string, payload: Record<string, unknown>): boolean {
1675
3163
  if (type === "goal.completed") {
1676
3164
  return true;
1677
3165
  }
1678
- if (type === "goal.set" || type === "goal.updated" || type === "goal.paused") {
3166
+ if (
3167
+ type === "goal.set" ||
3168
+ type === "goal.updated" ||
3169
+ type === "goal.paused" ||
3170
+ type === "goal.held"
3171
+ ) {
1679
3172
  return payload.actor === "agent";
1680
3173
  }
1681
3174
  return false;
@@ -1746,7 +3239,7 @@ function capabilityAuthorizationRequest(
1746
3239
  if (
1747
3240
  typeof record.id !== "string" ||
1748
3241
  typeof record.name !== "string" ||
1749
- !["pack", "mcp", "api", "skill", "plugin"].includes(String(kind)) ||
3242
+ !["mcp", "api", "skill", "plugin"].includes(String(kind)) ||
1750
3243
  !["built_in", "library", "configured", "public_registry", "registry", "manual"].includes(
1751
3244
  String(source),
1752
3245
  ) ||
@@ -1766,6 +3259,29 @@ function capabilityAuthorizationRequest(
1766
3259
  };
1767
3260
  }
1768
3261
 
3262
+ function customMcpSetupRequest(value: unknown): NonNullable<AuthNeededItem["setupRequest"]> | null {
3263
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
3264
+ const input = value as Record<string, unknown>;
3265
+ if (
3266
+ input.kind !== "mcp" ||
3267
+ typeof input.name !== "string" ||
3268
+ typeof input.rationale !== "string" ||
3269
+ typeof input.endpointUrl !== "string"
3270
+ )
3271
+ return null;
3272
+ try {
3273
+ if (new URL(input.endpointUrl).protocol !== "https:") return null;
3274
+ } catch {
3275
+ return null;
3276
+ }
3277
+ return {
3278
+ kind: "mcp",
3279
+ name: input.name,
3280
+ endpointUrl: input.endpointUrl,
3281
+ rationale: input.rationale,
3282
+ };
3283
+ }
3284
+
1769
3285
  function stringList(value: unknown): string[] {
1770
3286
  return Array.isArray(value)
1771
3287
  ? value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)