@openchambery/web 2.0.0-beta.1 → 2.0.0-beta.4

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 (287) hide show
  1. package/README.md +15 -11
  2. package/bin/cli-opencode.test.js +4 -4
  3. package/bin/cli.js +1 -1
  4. package/bin/lib/cli-startup.js +1 -1
  5. package/bin/lib/cli-startup.test.js +18 -4
  6. package/dist/assets/{AgentAvatar-CLWWNu_Q.js → AgentAvatar-Q1SmFRx2.js} +1 -1
  7. package/dist/assets/AssistantView-CwXkiCAd.js +3 -0
  8. package/dist/assets/{DiagramView-xuSJTnv7.js → DiagramView-BW92schO.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-Ov0D96d0.js → FadeInOnReveal-BZmwl-zE.js} +1 -1
  10. package/dist/assets/{Icon-DD-BuMfZ.js → Icon-AQPvxMiU.js} +1 -1
  11. package/dist/assets/{MarkdownRenderer-bc_p1H7l.js → MarkdownRenderer-Bv05iDZZ.js} +2 -2
  12. package/dist/assets/{MarkdownRendererImpl-SKi4oG0c.js → MarkdownRendererImpl-C81xqYib.js} +3 -3
  13. package/dist/assets/MarkstreamRendererImpl-CXUCQuKJ.js +3 -0
  14. package/dist/assets/{MobileDetailNavigation-Bnx59U_J.js → MobileDetailNavigation-BMgIu7zc.js} +1 -1
  15. package/dist/assets/{MobileShareBridge-BQlP4J-F.js → MobileShareBridge-BOXGNHmC.js} +2 -2
  16. package/dist/assets/MobileSurface-xuc_z0_G.js +2 -0
  17. package/dist/assets/{MultiRunWindow-ChZGob0v.js → MultiRunWindow-B5At2Z95.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-DtQJxZG-.js → OnboardingScreen-bsPLgz4r.js} +1 -1
  19. package/dist/assets/OverlayScrollbar-4wp4migh.js +2 -0
  20. package/dist/assets/SettingsView-CJ6tcH5_.js +25 -0
  21. package/dist/assets/{SettingsWindow-DfnsFa8y.js → SettingsWindow-CMtX2bMl.js} +1 -1
  22. package/dist/assets/{TerminalView-Dh9MWCpS.js → TerminalView-Dvr-ysTS.js} +1 -1
  23. package/dist/assets/{ToolOutputDialog-BejmjrRk.js → ToolOutputDialog-uH1ZZDSS.js} +2 -2
  24. package/dist/assets/appThemeRegistry-DK7q2juD.js +1 -0
  25. package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
  26. package/dist/assets/{button-D-rATN7w.js → button-CcNJUxUd.js} +1 -1
  27. package/dist/assets/collapsible-ZmZ6rLWs.js +3 -0
  28. package/dist/assets/context-menu-B2LXJgXL.js +1 -0
  29. package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-C6HrP0jq.js} +1 -1
  30. package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DdVBp45U.js} +1 -1
  31. package/dist/assets/{diagram-DQf7Y_uR.js → diagram-CxTs9hCQ.js} +1 -1
  32. package/dist/assets/{dialog-bfbvR4VH.js → dialog-KMVL9QKF.js} +1 -1
  33. package/dist/assets/es-CN3E_zXj.js +15 -0
  34. package/dist/assets/{fileMentionTouchSelection-BvrNCroG.js → fileMentionTouchSelection-7RIwlpkB.js} +1 -1
  35. package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-CjdBRy8y.js} +1 -1
  36. package/dist/assets/fr-9zZvQZD2.js +13 -0
  37. package/dist/assets/gitApi-yuWg9BN4.js +10 -0
  38. package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-9vQBq3zp.js} +1 -1
  39. package/dist/assets/{input-8r6F_Fza.js → input-CrIvk3HZ.js} +1 -1
  40. package/dist/assets/insertionBoundaries-BiUCSkx-.js +1 -0
  41. package/dist/assets/ja-Dnjk_82N.js +15 -0
  42. package/dist/assets/ko-BxCEE99C.js +15 -0
  43. package/dist/assets/{main-CCE_4y5F.js → main-C9mIBo77.js} +2 -2
  44. package/dist/assets/main-DiL6w6KF.js +169 -0
  45. package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
  46. package/dist/assets/{markdownHeightCache-1Qo6y6IJ.js → markdownHeightCache-BmXVY_gc.js} +1 -1
  47. package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
  48. package/dist/assets/mcpQueries-C7jknHV8.js +1 -0
  49. package/dist/assets/miniChat-CwtUaElU.js +2 -0
  50. package/dist/assets/mobile-DB-6--ws.js +2 -0
  51. package/dist/assets/multirun-TAsxFI8e.js +4 -0
  52. package/dist/assets/{persistence-Ce3AnpEr.js → persistence-56kq8Z4V.js} +2 -2
  53. package/dist/assets/pl-CNNsWnPM.js +15 -0
  54. package/dist/assets/platform-C13fMVEs.js +6 -0
  55. package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
  56. package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
  57. package/dist/assets/renderElectronMiniChatApp-CGj0MNRU.js +2 -0
  58. package/dist/assets/renderMobileApp-CGMMqbsO.js +28 -0
  59. package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
  60. package/dist/assets/{runtimeConfig-BMvqa5we.js → runtimeConfig-BEANkvOd.js} +1 -1
  61. package/dist/assets/runtimeEndpointReset-A_ZBUJFS.js +147 -0
  62. package/dist/assets/sessionChangeSummary-qOLrP9fw.js +2 -0
  63. package/dist/assets/{sessionLookup-gh-hVRIc.js → sessionLookup-34XFmP9C.js} +1 -1
  64. package/dist/assets/{sortable-tabs-strip-CudoSgV4.js → sortable-tabs-strip-DyEV0Heg.js} +1 -1
  65. package/dist/assets/src-ubCWXZhR.css +2 -0
  66. package/dist/assets/store-DLCiZaA6.js +15 -0
  67. package/dist/assets/{terminalApi-DiF6JmBP.js → terminalApi-CXaBEoa_.js} +1 -1
  68. package/dist/assets/textarea-BBOlRI9O.js +1 -0
  69. package/dist/assets/{tooltip-CQvpK-UV.js → tooltip-CPI4fr1a.js} +1 -1
  70. package/dist/assets/uk-rwQrxeYM.js +15 -0
  71. package/dist/assets/{url-_z7evzdK.js → url-DRDQXjyX.js} +1 -1
  72. package/dist/assets/{useAppFontEffects-Cpmpo7Wb.js → useAppFontEffects-CiDe42I5.js} +57 -43
  73. package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
  74. package/dist/assets/useDesktopWindowControlsLayout-BlizAlOt.js +1 -0
  75. package/dist/assets/useEffectiveDirectory-C_CouL5v.js +1 -0
  76. package/dist/assets/{useFontPreferences-DcbXQOkc.js → useFontPreferences-B0oUZAiB.js} +1 -1
  77. package/dist/assets/useMobileNavigationStore-DfPClaW-.js +414 -0
  78. package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
  79. package/dist/assets/useSessionAutoCleanup-DtrfbxAp.js +2 -0
  80. package/dist/assets/{useThemeSystem-DMzWVJaW.js → useThemeSystem-BlNUWH47.js} +1 -1
  81. package/dist/assets/useUIStore-CQYKJnrg.js +1 -0
  82. package/dist/assets/{useWorkerHighlightedLines-Bqn4AiBL.js → useWorkerHighlightedLines-CPNYz6Wf.js} +2 -2
  83. package/dist/assets/{vendor-base-ui-CnHfUmDN.js → vendor-base-ui-CDjyofeD.js} +1 -1
  84. package/dist/assets/{vendor-cmdk-CeAGfrLo.js → vendor-cmdk-BFH0UpXX.js} +1 -1
  85. package/dist/assets/{vendor-dnd-kit-accessibility-BY5LhhUB.js → vendor-dnd-kit-accessibility-BrR3AKvM.js} +1 -1
  86. package/dist/assets/{vendor-dnd-kit-core-CvWxUPdb.js → vendor-dnd-kit-core-CNfdvk6H.js} +1 -1
  87. package/dist/assets/{vendor-dnd-kit-sortable-hHdP9lLb.js → vendor-dnd-kit-sortable-DQUaTeGj.js} +1 -1
  88. package/dist/assets/{vendor-framer-motion-DFy_BSVd.js → vendor-framer-motion-4dogsUk3.js} +1 -1
  89. package/dist/assets/{vendor-legendapp-list-WE32lwNk.js → vendor-legendapp-list-BU7MdPZR.js} +1 -1
  90. package/dist/assets/{vendor-markstream-react-DX55XLGO.js → vendor-markstream-react-qHC2FykU.js} +3 -3
  91. package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
  92. package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
  93. package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
  94. package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
  95. package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
  96. package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
  97. package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
  98. package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
  99. package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
  100. package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
  101. package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
  102. package/dist/index.html +13 -13
  103. package/dist/mini-chat.html +13 -13
  104. package/dist/mobile.html +13 -13
  105. package/package.json +1 -1
  106. package/server/index.js +327 -62
  107. package/server/lib/assistants/DOCUMENTATION.md +14 -9
  108. package/server/lib/assistants/contracts.js +11 -1
  109. package/server/lib/assistants/projection-history.test.js +265 -18
  110. package/server/lib/assistants/routes.js +6 -0
  111. package/server/lib/assistants/service.js +417 -231
  112. package/server/lib/assistants/service.test.js +392 -148
  113. package/server/lib/conversations/DOCUMENTATION.md +1 -1
  114. package/server/lib/conversations/service.js +1 -0
  115. package/server/lib/conversations/service.test.js +16 -0
  116. package/server/lib/conversations/validation.js +11 -1
  117. package/server/lib/event-stream/DOCUMENTATION.md +3 -1
  118. package/server/lib/event-stream/directory-ws-bridge.js +13 -2
  119. package/server/lib/event-stream/global-hub.js +49 -4
  120. package/server/lib/event-stream/reasoning-projection.js +16 -1
  121. package/server/lib/event-stream/reasoning-projection.test.js +15 -0
  122. package/server/lib/event-stream/runtime.js +57 -20
  123. package/server/lib/event-stream/runtime.test.js +112 -0
  124. package/server/lib/llm/DOCUMENTATION.md +33 -9
  125. package/server/lib/llm/catalog.js +1 -1
  126. package/server/lib/llm/completions.js +5 -1
  127. package/server/lib/llm/completions.test.js +3 -3
  128. package/server/lib/llm/generate.js +237 -145
  129. package/server/lib/llm/generate.test.js +201 -46
  130. package/server/lib/llm/routes.js +2 -0
  131. package/server/lib/message-queue/runtime.js +15 -2
  132. package/server/lib/notifications/runtime.js +23 -5
  133. package/server/lib/notifications/runtime.test.js +11 -0
  134. package/server/lib/opencode/DOCUMENTATION.md +85 -17
  135. package/server/lib/opencode/agent-document.d.ts +28 -0
  136. package/server/lib/opencode/agent-document.js +280 -0
  137. package/server/lib/opencode/agent-document.test.js +137 -0
  138. package/server/lib/opencode/agents.js +96 -0
  139. package/server/lib/opencode/auth-state-runtime.js +5 -0
  140. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  141. package/server/lib/opencode/config-entity-routes.js +42 -99
  142. package/server/lib/opencode/config-entity-routes.test.js +107 -0
  143. package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
  144. package/server/lib/opencode/ensure-cli.js +1 -2
  145. package/server/lib/opencode/env-runtime.js +40 -26
  146. package/server/lib/opencode/env-runtime.test.js +10 -17
  147. package/server/lib/opencode/feature-routes-runtime.js +43 -4
  148. package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
  149. package/server/lib/opencode/http-server-connections.js +98 -0
  150. package/server/lib/opencode/http-server-connections.test.js +321 -0
  151. package/server/lib/opencode/lifecycle.js +349 -39
  152. package/server/lib/opencode/lifecycle.test.js +322 -0
  153. package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
  154. package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
  155. package/server/lib/opencode/openchamber-routes.js +7 -1
  156. package/server/lib/opencode/opencode2-pin.js +4 -4
  157. package/server/lib/opencode/opencode2-pin.test.js +1 -0
  158. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +127 -4
  159. package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
  160. package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
  161. package/server/lib/opencode/plugin-routes.js +3 -18
  162. package/server/lib/opencode/plugin-routes.test.js +9 -9
  163. package/server/lib/opencode/provider-catalog.js +58 -12
  164. package/server/lib/opencode/provider-catalog.test.js +48 -0
  165. package/server/lib/opencode/proxy.js +252 -46
  166. package/server/lib/opencode/proxy.test.js +235 -2
  167. package/server/lib/opencode/routes.js +296 -62
  168. package/server/lib/opencode/routes.test.js +278 -12
  169. package/server/lib/opencode/runtime-contract.js +401 -0
  170. package/server/lib/opencode/runtime-contract.test.js +214 -0
  171. package/server/lib/opencode/server-opencode-fetch.js +242 -0
  172. package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
  173. package/server/lib/opencode/server-utils-runtime.js +33 -0
  174. package/server/lib/opencode/shared-service.integration.test.js +66 -0
  175. package/server/lib/opencode/shared-service.js +147 -0
  176. package/server/lib/opencode/shared-service.test.js +93 -0
  177. package/server/lib/opencode/shared.js +3 -3
  178. package/server/lib/opencode/shutdown-runtime.js +15 -6
  179. package/server/lib/opencode/shutdown-runtime.test.js +49 -1
  180. package/server/lib/opencode/skill-routes.js +20 -33
  181. package/server/lib/opencode/skill-routes.test.js +35 -2
  182. package/server/lib/opencode/skills.test.js +15 -0
  183. package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
  184. package/server/lib/opencode/static-routes-runtime.js +3 -1
  185. package/server/lib/opencode/static-routes-runtime.test.js +33 -0
  186. package/server/lib/opencode/v2-client.js +14 -3
  187. package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
  188. package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
  189. package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
  190. package/server/lib/permission-auto-accept/runtime.js +14 -7
  191. package/server/lib/permission-auto-accept/runtime.test.js +27 -12
  192. package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
  193. package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -0
  194. package/server/lib/scheduled-tasks/runtime.js +52 -4
  195. package/server/lib/scheduled-tasks/runtime.test.js +80 -1
  196. package/server/lib/session-goal/DOCUMENTATION.md +113 -13
  197. package/server/lib/session-goal/routes.js +9 -0
  198. package/server/lib/session-goal/runtime.js +1579 -117
  199. package/server/lib/session-goal/runtime.test.js +1453 -84
  200. package/server/lib/session-index/DOCUMENTATION.md +18 -15
  201. package/server/lib/session-index/event-ingest.js +89 -16
  202. package/server/lib/session-index/event-ingest.test.js +73 -0
  203. package/server/lib/session-index/host-authority-snapshot.js +92 -0
  204. package/server/lib/session-index/routes.host-archive.test.js +201 -0
  205. package/server/lib/session-index/routes.js +63 -10
  206. package/server/lib/session-index/routes.test.js +8 -8
  207. package/server/lib/session-index/service.js +7 -1
  208. package/server/lib/session-index/sync-runtime.js +161 -34
  209. package/server/lib/session-index/sync-runtime.test.js +155 -1
  210. package/server/lib/session-metadata/DOCUMENTATION.md +61 -16
  211. package/server/lib/session-metadata/routes.js +108 -9
  212. package/server/lib/session-metadata/routes.test.js +47 -2
  213. package/server/lib/session-metadata/session-archive.d.ts +73 -0
  214. package/server/lib/session-metadata/session-archive.js +654 -0
  215. package/server/lib/session-metadata/session-archive.test.js +359 -0
  216. package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
  217. package/server/lib/session-metadata/session-metadata-store.js +212 -72
  218. package/server/lib/session-metadata/session-metadata-store.test.js +205 -10
  219. package/server/lib/session-metadata/session-projection.d.ts +41 -0
  220. package/server/lib/session-metadata/session-projection.js +255 -0
  221. package/server/lib/session-metadata/session-projection.test.js +64 -0
  222. package/server/lib/session-metadata/system-session.js +55 -0
  223. package/server/lib/session-metadata/system-session.test.js +55 -0
  224. package/server/lib/session-title/DOCUMENTATION.md +40 -7
  225. package/server/lib/session-title/native-runtime.test.js +167 -0
  226. package/server/lib/session-title/runtime.js +67 -42
  227. package/server/lib/session-title/session-access.js +46 -0
  228. package/server/lib/session-title/session-access.test.js +91 -0
  229. package/server/lib/session-turn-pages/DOCUMENTATION.md +9 -8
  230. package/server/lib/session-turn-pages/routes.js +17 -102
  231. package/server/lib/session-turn-pages/routes.test.js +281 -0
  232. package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
  233. package/server/lib/session-turn-pages/session-message-projection.js +281 -0
  234. package/server/lib/small-model/DOCUMENTATION.md +101 -147
  235. package/server/lib/small-model/catalog.js +31 -51
  236. package/server/lib/small-model/catalog.test.js +65 -10
  237. package/server/lib/small-model/custom-api.js +68 -0
  238. package/server/lib/small-model/custom-api.test.js +64 -0
  239. package/server/lib/small-model/index.js +124 -240
  240. package/server/lib/small-model/resolve.js +35 -120
  241. package/server/lib/small-model/resolve.test.js +20 -110
  242. package/server/lib/small-model/routes.js +1 -3
  243. package/server/lib/small-model/summary-settings.test.js +157 -253
  244. package/server/opencode-proxy.test.js +2 -0
  245. package/dist/assets/AssistantView-BTCkm8yk.js +0 -3
  246. package/dist/assets/MarkstreamRendererImpl-CnkodGJW.js +0 -3
  247. package/dist/assets/MobileSurface-BXLeiZ4d.js +0 -2
  248. package/dist/assets/OverlayScrollbar-pMwxaCca.js +0 -2
  249. package/dist/assets/SettingsView-B5I0KDTZ.js +0 -25
  250. package/dist/assets/appThemeRegistry-COGfxTFV.js +0 -1
  251. package/dist/assets/collapsible-BXRd5_Hr.js +0 -1
  252. package/dist/assets/context-menu-jGtH6fR9.js +0 -1
  253. package/dist/assets/es-c52LTHHe.js +0 -15
  254. package/dist/assets/fr-PisFbUE8.js +0 -13
  255. package/dist/assets/gitApi-ButwzQ0N.js +0 -9
  256. package/dist/assets/insertionBoundaries-DGs7S5zv.js +0 -1
  257. package/dist/assets/ja-C8Ikun3Z.js +0 -15
  258. package/dist/assets/ko-ClFys5o2.js +0 -15
  259. package/dist/assets/main-Dn9rpsgl.js +0 -169
  260. package/dist/assets/markdown-worker-CptoICmq.js +0 -2
  261. package/dist/assets/miniChat-BpjxVqgg.js +0 -2
  262. package/dist/assets/mobile-nMJCaNq1.js +0 -2
  263. package/dist/assets/multirun-BvSG94mW.js +0 -4
  264. package/dist/assets/pl-CnaKWN5F.js +0 -15
  265. package/dist/assets/platform-CWTIp02q.js +0 -4
  266. package/dist/assets/projectMeta-DOe49dM_.js +0 -3
  267. package/dist/assets/pt-BR-s8OpfRI6.js +0 -15
  268. package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +0 -2
  269. package/dist/assets/renderMobileApp-y-S8y8v_.js +0 -28
  270. package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
  271. package/dist/assets/runtimeEndpointReset-B6BxhvR0.js +0 -147
  272. package/dist/assets/sessionChangeSummary-VVBlEXuh.js +0 -2
  273. package/dist/assets/src-BzX5UI-c.css +0 -2
  274. package/dist/assets/store-DJczmczv.js +0 -15
  275. package/dist/assets/textarea-3XR8sUfy.js +0 -1
  276. package/dist/assets/uk-CaX0iqkB.js +0 -15
  277. package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +0 -1
  278. package/dist/assets/useEffectiveDirectory-CN6JoCFw.js +0 -1
  279. package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +0 -414
  280. package/dist/assets/useSessionAutoCleanup-BLjymaFt.js +0 -2
  281. package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
  282. package/dist/assets/zh-CN-20MYEjha.js +0 -15
  283. package/dist/assets/zh-TW-DiN04qTw.js +0 -15
  284. package/server/lib/small-model/call.js +0 -427
  285. package/server/lib/small-model/call.test.js +0 -382
  286. package/server/lib/small-model/opencode-session.js +0 -183
  287. package/server/lib/small-model/opencode-session.test.js +0 -147
@@ -72,60 +72,103 @@ describe('generate cancellation', () => {
72
72
  expect(interrupt).toHaveBeenCalledWith({ sessionID: 'ses_abort_fixture', continue: false })
73
73
  })
74
74
 
75
- it('aborts an in-flight generate.text when the contact continuation is cancelled', async () => {
75
+ it('aborts an in-flight session.generate and still removes the throwaway session', async () => {
76
76
  const controller = new AbortController()
77
- const text = vi.fn(async (_args, { signal }) => {
77
+ const generate = vi.fn(async (_args, { signal }) => {
78
78
  controller.abort()
79
79
  signal.throwIfAborted()
80
80
  })
81
+ const { client, remove, interrupt } = textSessionClient({ generate })
81
82
  await expect(generateOpenCodeText({
82
83
  buildOpenCodeUrl: () => 'http://127.0.0.1:4096',
83
84
  getOpenCodeAuthHeaders: () => ({}),
84
85
  providerID: 'opencode',
85
86
  modelID: 'gpt-5-nano',
86
87
  messages: [{ role: 'user', content: 'work' }],
87
- clientFactory: () => ({ generate: { text } }),
88
+ clientFactory: () => client,
89
+ ensureTempDirectory: async () => '/tmp/openchamber-text',
88
90
  signal: controller.signal,
89
91
  })).rejects.toMatchObject({ name: 'AbortError' })
90
- expect(text).toHaveBeenCalledTimes(1)
92
+ expect(generate).toHaveBeenCalledTimes(1)
93
+ expect(interrupt).toHaveBeenCalledWith({ sessionID: 'ses_text', continue: false })
94
+ expect(remove).toHaveBeenCalledExactlyOnceWith({ sessionID: 'ses_text' })
91
95
  })
92
96
  })
93
97
 
98
+ const denyAllTextAgent = {
99
+ location: { directory: '/tmp/openchamber-text' },
100
+ data: {
101
+ id: 'openchamber-text',
102
+ name: 'openchamber-text',
103
+ mode: 'primary',
104
+ hidden: true,
105
+ permissions: [{ action: '*', resource: '*', effect: 'deny' }],
106
+ },
107
+ }
108
+
109
+ function textSessionClient({ generate = vi.fn(async () => ({ text: 'reply' })), agentGet } = {}) {
110
+ const create = vi.fn(async () => ({ id: 'ses_text' }))
111
+ const remove = vi.fn(async () => undefined)
112
+ const interrupt = vi.fn(async () => undefined)
113
+ const prompt = vi.fn()
114
+ const get = agentGet || vi.fn(async () => denyAllTextAgent)
115
+ return {
116
+ client: {
117
+ agent: { get },
118
+ session: { create, generate, remove, interrupt, prompt },
119
+ },
120
+ agentGet: get,
121
+ create,
122
+ generate,
123
+ remove,
124
+ interrupt,
125
+ prompt,
126
+ }
127
+ }
128
+
94
129
  describe('generateOpenCodeText — text path', () => {
95
- it('uses generate.text with model id/providerID and never opens a session', async () => {
96
- const text = vi.fn(async () => ({ text: 'reply' }))
97
- const create = vi.fn()
98
- const prompt = vi.fn()
99
- const clientFactory = () => ({
100
- generate: { text },
101
- session: { create, prompt },
102
- agent: { get: vi.fn() },
130
+ it('runs session.generate in a verified deny-all throwaway session and removes it', async () => {
131
+ const { client, agentGet, create, generate, remove, prompt } = textSessionClient()
132
+ const ensureTempDirectory = vi.fn(async ({ agentName, agentMarkdown }) => {
133
+ expect(agentName).toBe(_test.TEXT_AGENT_NAME)
134
+ expect(agentMarkdown).toBe(_test.TEXT_AGENT_MARKDOWN)
135
+ return '/tmp/openchamber-text'
103
136
  })
104
137
 
105
138
  const result = await generateOpenCodeText({
106
139
  buildOpenCodeUrl: () => 'http://127.0.0.1:4096',
107
140
  getOpenCodeAuthHeaders: () => ({}),
108
- providerID: 'opencode',
109
- modelID: 'gpt-5-nano',
141
+ providerID: 'opencode-go',
142
+ modelID: 'deepseek-v4-flash',
110
143
  messages: [
111
144
  { role: 'system', content: 'Be brief' },
112
145
  { role: 'user', content: 'hi' },
113
146
  ],
114
- clientFactory,
147
+ clientFactory: () => client,
148
+ ensureTempDirectory,
115
149
  })
116
150
 
117
- expect(result).toEqual({ text: 'reply', source: 'generate.text' })
118
- expect(text).toHaveBeenCalledWith(expect.objectContaining({
119
- prompt: expect.stringContaining('Be brief'),
120
- model: { id: 'gpt-5-nano', providerID: 'opencode' },
151
+ expect(result).toEqual({ text: 'reply', source: 'session.generate' })
152
+ expect(agentGet).toHaveBeenCalledWith(expect.objectContaining({
153
+ agentID: 'openchamber-text',
154
+ location: { directory: '/tmp/openchamber-text' },
121
155
  }), expect.anything())
122
- expect(text.mock.calls[0][0].prompt).toContain('User: hi')
123
- expect(create).not.toHaveBeenCalled()
156
+ expect(create).toHaveBeenCalledWith(expect.objectContaining({
157
+ agent: 'openchamber-text',
158
+ model: { id: 'deepseek-v4-flash', providerID: 'opencode-go' },
159
+ location: { directory: '/tmp/openchamber-text' },
160
+ }), expect.anything())
161
+ expect(agentGet.mock.invocationCallOrder[0]).toBeLessThan(create.mock.invocationCallOrder[0])
162
+ const generateArgs = generate.mock.calls[0][0]
163
+ expect(generateArgs.sessionID).toBe('ses_text')
164
+ expect(generateArgs.prompt.startsWith('Be brief')).toBe(true)
165
+ expect(generateArgs.prompt).toContain('User: hi')
124
166
  expect(prompt).not.toHaveBeenCalled()
167
+ expect(remove).toHaveBeenCalledExactlyOnceWith({ sessionID: 'ses_text' })
125
168
  })
126
169
 
127
- it.each(['high', undefined])('passes variant %s to generate.text and returns only text', async (variant) => {
128
- const text = vi.fn(async () => ({ text: 'public reply' }))
170
+ it.each(['high', undefined])('passes variant %s on the session model', async (variant) => {
171
+ const { client, create } = textSessionClient()
129
172
  const result = await generateOpenCodeText({
130
173
  buildOpenCodeUrl: () => 'http://127.0.0.1:4096',
131
174
  getOpenCodeAuthHeaders: () => ({}),
@@ -133,10 +176,11 @@ describe('generateOpenCodeText — text path', () => {
133
176
  modelID: 'gpt-5-nano',
134
177
  messages: [{ role: 'user', content: 'hi' }],
135
178
  variant,
136
- clientFactory: () => ({ generate: { text } }),
179
+ clientFactory: () => client,
180
+ ensureTempDirectory: async () => '/tmp/openchamber-text',
137
181
  })
138
- expect(result.text).toBe('public reply')
139
- const model = text.mock.calls[0][0].model
182
+ expect(result.text).toBe('reply')
183
+ const model = create.mock.calls[0][0].model
140
184
  if (variant) {
141
185
  expect(model).toEqual({ id: 'gpt-5-nano', providerID: 'opencode', variant })
142
186
  } else {
@@ -144,7 +188,85 @@ describe('generateOpenCodeText — text path', () => {
144
188
  }
145
189
  })
146
190
 
147
- it('does not invent deltas on the generate.text path', async () => {
191
+ it('blocks before creating a session when the text agent is not deny-all', async () => {
192
+ const { client, create, generate } = textSessionClient({
193
+ agentGet: vi.fn(async () => ({
194
+ data: { id: 'openchamber-text', permissions: [{ action: 'bash', resource: '*', effect: 'allow' }] },
195
+ })),
196
+ })
197
+ await expect(generateOpenCodeText({
198
+ buildOpenCodeUrl: () => 'http://127.0.0.1:4096',
199
+ getOpenCodeAuthHeaders: () => ({}),
200
+ providerID: 'opencode',
201
+ modelID: 'gpt-5-nano',
202
+ messages: [{ role: 'user', content: 'hi' }],
203
+ clientFactory: () => client,
204
+ ensureTempDirectory: async () => '/tmp/openchamber-text',
205
+ })).rejects.toMatchObject({ code: 'llm_attachment_generation_unavailable' })
206
+ expect(create).not.toHaveBeenCalled()
207
+ expect(generate).not.toHaveBeenCalled()
208
+ })
209
+
210
+ it('waits for a lazily loaded agent instead of failing on the first read', async () => {
211
+ let reads = 0
212
+ const agentGet = vi.fn(async () => {
213
+ reads += 1
214
+ if (reads < 3) throw new Error('Agent not found: openchamber-text')
215
+ return denyAllTextAgent
216
+ })
217
+ const client = { agent: { get: agentGet } }
218
+ const agent = await _test.assertLlmAgentDenyAll({
219
+ client,
220
+ location: { directory: '/tmp/openchamber-text' },
221
+ agentID: 'openchamber-text',
222
+ waitMs: 1_000,
223
+ pollMs: 1,
224
+ })
225
+ expect(agent.id).toBe('openchamber-text')
226
+ expect(agentGet).toHaveBeenCalledTimes(3)
227
+ })
228
+
229
+ it('fails when the agent never appears within the wait window', async () => {
230
+ const agentGet = vi.fn(async () => { throw new Error('Agent not found') })
231
+ await expect(_test.assertLlmAgentDenyAll({
232
+ client: { agent: { get: agentGet } },
233
+ agentID: 'openchamber-text',
234
+ waitMs: 0,
235
+ pollMs: 1,
236
+ })).rejects.toMatchObject({ code: 'llm_attachment_generation_unavailable' })
237
+ expect(agentGet).toHaveBeenCalledTimes(1)
238
+ })
239
+
240
+ it('surfaces session.generate errors and still removes the session', async () => {
241
+ const { client, remove } = textSessionClient({
242
+ generate: vi.fn(async () => { throw new Error('Request is missing x-opencode-session') }),
243
+ })
244
+ await expect(generateOpenCodeText({
245
+ buildOpenCodeUrl: () => 'http://127.0.0.1:4096',
246
+ getOpenCodeAuthHeaders: () => ({}),
247
+ providerID: 'opencode-go',
248
+ modelID: 'deepseek-v4-flash',
249
+ messages: [{ role: 'user', content: 'hi' }],
250
+ clientFactory: () => client,
251
+ ensureTempDirectory: async () => '/tmp/openchamber-text',
252
+ })).rejects.toMatchObject({ code: 'upstream_error', message: 'Request is missing x-opencode-session' })
253
+ expect(remove).toHaveBeenCalledWith({ sessionID: 'ses_text' })
254
+ })
255
+
256
+ it('rejects an empty session.generate reply instead of returning blank success', async () => {
257
+ const { client } = textSessionClient({ generate: vi.fn(async () => ({ text: ' ' })) })
258
+ await expect(generateOpenCodeText({
259
+ buildOpenCodeUrl: () => 'http://127.0.0.1:4096',
260
+ getOpenCodeAuthHeaders: () => ({}),
261
+ providerID: 'opencode',
262
+ modelID: 'gpt-5-nano',
263
+ messages: [{ role: 'user', content: 'hi' }],
264
+ clientFactory: () => client,
265
+ ensureTempDirectory: async () => '/tmp/openchamber-text',
266
+ })).rejects.toMatchObject({ message: 'OpenCode session.generate returned no text' })
267
+ })
268
+
269
+ it('does not invent deltas on the session.generate path', async () => {
148
270
  const onTextDelta = vi.fn()
149
271
  const subscribers = new Set()
150
272
  const globalEventHub = {
@@ -153,19 +275,19 @@ describe('generateOpenCodeText — text path', () => {
153
275
  return () => { subscribers.delete(fn) }
154
276
  },
155
277
  }
278
+ const { client } = textSessionClient({ generate: vi.fn(async () => ({ text: 'full reply' })) })
156
279
  const result = await generateOpenCodeText({
157
280
  buildOpenCodeUrl: () => 'http://127.0.0.1:4096',
158
281
  getOpenCodeAuthHeaders: () => ({}),
159
282
  providerID: 'opencode',
160
283
  modelID: 'gpt-5-nano',
161
284
  messages: [{ role: 'user', content: 'hi' }],
162
- clientFactory: () => ({
163
- generate: { text: async () => ({ text: 'full reply' }) },
164
- }),
285
+ clientFactory: () => client,
286
+ ensureTempDirectory: async () => '/tmp/openchamber-text',
165
287
  onTextDelta,
166
288
  globalEventHub,
167
289
  })
168
- expect(result).toEqual({ text: 'full reply', source: 'generate.text' })
290
+ expect(result).toEqual({ text: 'full reply', source: 'session.generate' })
169
291
  expect(onTextDelta).not.toHaveBeenCalled()
170
292
  expect(subscribers.size).toBe(0)
171
293
  })
@@ -176,23 +298,20 @@ describe('generateOpenCodeText — text path', () => {
176
298
  expect(_test.filesForPrompt([image, file], false)).toEqual([file])
177
299
  expect(_test.imageFilesForSession([image, file], false)).toEqual([])
178
300
 
179
- const text = vi.fn(async () => ({ text: 'cannot see images' }))
180
- const create = vi.fn()
301
+ const { client, generate, prompt } = textSessionClient({ generate: vi.fn(async () => ({ text: 'cannot see images' })) })
181
302
  await generateOpenCodeText({
182
303
  buildOpenCodeUrl: () => 'http://127.0.0.1:4096',
183
304
  getOpenCodeAuthHeaders: () => ({}),
184
305
  providerID: 'opencode',
185
306
  modelID: 'deepseek-v4-flash',
186
307
  messages: [{ role: 'user', content: 'look', parts: [image, file] }],
187
- clientFactory: () => ({
188
- generate: { text },
189
- session: { create },
190
- }),
308
+ clientFactory: () => client,
309
+ ensureTempDirectory: async () => '/tmp/openchamber-text',
191
310
  forwardImageParts: false,
192
311
  })
193
- expect(create).not.toHaveBeenCalled()
194
- expect(text.mock.calls[0][0].prompt).toContain('[image: shot.png (image/png)]')
195
- expect(text.mock.calls[0][0].prompt).toContain('[file: notes.txt (text/plain)]')
312
+ expect(prompt).not.toHaveBeenCalled()
313
+ expect(generate.mock.calls[0][0].prompt).toContain('[image: shot.png (image/png)]')
314
+ expect(generate.mock.calls[0][0].prompt).toContain('[file: notes.txt (text/plain)]')
196
315
  })
197
316
  })
198
317
 
@@ -207,6 +326,8 @@ describe('generateOpenCodeText — attachment session path', () => {
207
326
  const remove = vi.fn(async () => undefined)
208
327
  const interrupt = vi.fn(async () => undefined)
209
328
  const text = vi.fn()
329
+ const persistSessionMetadata = vi.fn(async () => ({}))
330
+ const onSystemSessionPersisted = vi.fn()
210
331
  const image = { type: 'file', mime: 'image/png', url: 'data:image/png;base64,aa', filename: 'shot.png' }
211
332
 
212
333
  const result = await generateOpenCodeText({
@@ -219,6 +340,8 @@ describe('generateOpenCodeText — attachment session path', () => {
219
340
  { role: 'user', content: 'look', parts: [image] },
220
341
  ],
221
342
  variant: 'high',
343
+ persistSessionMetadata,
344
+ onSystemSessionPersisted,
222
345
  clientFactory: () => ({
223
346
  generate: { text },
224
347
  agent: { get: agentGet },
@@ -250,6 +373,7 @@ describe('generateOpenCodeText — attachment session path', () => {
250
373
  agent: 'openchamber-llm',
251
374
  model: { id: 'gpt-4o', providerID: 'opencode', variant: 'high' },
252
375
  location: { directory: '/tmp/openchamber-llm' },
376
+ metadata: { openchamber: { llm: { purpose: 'chat-completions' } } },
253
377
  }), expect.anything())
254
378
  expect(put).toHaveBeenCalledWith(expect.objectContaining({
255
379
  sessionID: 'ses_tmp',
@@ -270,6 +394,14 @@ describe('generateOpenCodeText — attachment session path', () => {
270
394
  order: 'desc',
271
395
  }), expect.anything())
272
396
  expect(remove).toHaveBeenCalledWith({ sessionID: 'ses_tmp' })
397
+ expect(persistSessionMetadata).toHaveBeenCalledWith('ses_tmp', {
398
+ openchamber: { llm: { purpose: 'chat-completions' } },
399
+ })
400
+ expect(onSystemSessionPersisted).toHaveBeenCalledWith({
401
+ sessionID: 'ses_tmp',
402
+ directory: '/tmp/openchamber-llm',
403
+ metadata: { openchamber: { llm: { purpose: 'chat-completions' } } },
404
+ })
273
405
  expect(text).not.toHaveBeenCalled()
274
406
  })
275
407
 
@@ -581,17 +713,39 @@ describe('data URL validation', () => {
581
713
  })
582
714
 
583
715
  describe('generateOpenCodeText — real OpenCode.make + fake HTTP', () => {
584
- it('catalog-shaped generate.text completion through the real client', async () => {
716
+ it('session.generate text completion through the real client', async () => {
585
717
  const { OpenCode } = await import('@opencode/client')
718
+ const calls = []
719
+ const emptyOk = { ok: true, status: 204, headers: { get: () => null }, text: async () => '', json: async () => null, arrayBuffer: async () => new ArrayBuffer(0) }
586
720
  const fetchImpl = vi.fn(async (url, init) => {
587
721
  const path = String(url)
588
- if (path.includes('/api/experimental/generate') && init?.method === 'POST') {
722
+ const method = init?.method || 'GET'
723
+ calls.push(`${method} ${path.replace(/^https?:\/\/[^/]+/, '').split('?')[0]}`)
724
+ if (path.includes('/api/agent/openchamber-text')) {
725
+ return jsonResponse(200, {
726
+ location: { directory: '/tmp/openchamber-text', project: { id: 'p', directory: '/tmp', canonical: '/tmp' } },
727
+ data: { ...denyAllTextAgent.data, request: {} },
728
+ })
729
+ }
730
+ if (path.endsWith('/api/session') && method === 'POST') {
731
+ return jsonResponse(200, {
732
+ data: {
733
+ id: 'ses_text',
734
+ projectID: 'p',
735
+ cost: 0,
736
+ tokens: {},
737
+ time: { created: 1, updated: 1 },
738
+ location: { directory: '/tmp/openchamber-text' },
739
+ },
740
+ })
741
+ }
742
+ if (path.includes('/api/session/ses_text/generate') && method === 'POST') {
589
743
  const body = JSON.parse(init.body)
590
- expect(body.model).toEqual({ id: 'gpt-5-nano', providerID: 'opencode' })
591
744
  expect(body.prompt).toContain('User: hi')
592
745
  return jsonResponse(200, { data: { text: 'hello from generate' } })
593
746
  }
594
- return jsonResponse(500, { _tag: 'UnknownError', message: `unexpected ${path}` })
747
+ if (path.includes('/api/session/ses_text') && method === 'DELETE') return emptyOk
748
+ return jsonResponse(500, { _tag: 'UnknownError', message: `unexpected ${method} ${path}` })
595
749
  })
596
750
 
597
751
  const result = await generateOpenCodeText({
@@ -605,10 +759,11 @@ describe('generateOpenCodeText — real OpenCode.make + fake HTTP', () => {
605
759
  headers: { Authorization: 'Basic test' },
606
760
  fetch: fetchImpl,
607
761
  }),
762
+ ensureTempDirectory: async () => '/tmp/openchamber-text',
608
763
  })
609
764
 
610
- expect(result).toEqual({ text: 'hello from generate', source: 'generate.text' })
611
- expect(fetchImpl).toHaveBeenCalled()
765
+ expect(result).toEqual({ text: 'hello from generate', source: 'session.generate' })
766
+ expect(calls.some((c) => c.startsWith('DELETE ') && c.includes('/api/session/ses_text'))).toBe(true)
612
767
  })
613
768
 
614
769
  it('attachment deny-all failure happens before prompt on the real client path shape', async () => {
@@ -40,6 +40,8 @@ export const registerLlmRoutes = (app, dependencies) => {
40
40
  getOpenCodeAuthHeaders: dependencies.getOpenCodeAuthHeaders,
41
41
  clientFactory: client,
42
42
  ensureTempDirectory: ensureLlmTempDirectory,
43
+ persistSessionMetadata: dependencies.persistSessionMetadata,
44
+ onSystemSessionPersisted: dependencies.onSystemSessionPersisted,
43
45
  }))
44
46
  .then(({ completion }) => {
45
47
  res.status(200).json(completion);
@@ -35,13 +35,26 @@ export const createMessageQueueRuntime = ({ dbPath, attachmentRoot, service, att
35
35
  const observeSessionEvent = (event) => {
36
36
  if (!isCurrentRuntimeEvent(event)) return false;
37
37
  const payload = event?.payload?.payload ?? event?.payload;
38
- const properties = payload?.properties;
38
+ // Accept bridge `{ properties }` and native v2 `{ data }` envelopes.
39
+ const properties = (payload?.properties && typeof payload.properties === 'object')
40
+ ? payload.properties
41
+ : ((payload?.data && typeof payload.data === 'object') ? payload.data : null);
39
42
  const directory = typeof event?.directory === 'string' && event.directory !== 'global' ? event.directory : '';
40
43
  const sessionID = typeof properties?.sessionID === 'string' ? properties.sessionID : '';
41
44
  if (!directory || !sessionID) return false;
45
+ // Ticket 07: shutdown interrupt keeps the upstream claim — do not treat as
46
+ // idle/error settlement for queue wake; wait for authoritative terminal.
47
+ if (payload.type === 'session.execution.interrupted' && properties?.reason === 'shutdown') {
48
+ return false;
49
+ }
42
50
  const phase = payload.type === 'session.status'
43
51
  ? properties?.status?.type ?? properties?.info?.type
44
- : payload.type === 'session.idle' ? 'idle' : payload.type === 'session.error' ? 'error' : '';
52
+ : payload.type === 'session.idle' ? 'idle'
53
+ : payload.type === 'session.error' ? 'error'
54
+ : payload.type === 'session.execution.succeeded' ? 'idle'
55
+ : payload.type === 'session.execution.failed' ? 'error'
56
+ : payload.type === 'session.execution.interrupted' ? 'idle'
57
+ : '';
45
58
  if (typeof phase !== 'string' || !phase) return false;
46
59
  openCodeAdapter.observeSessionEvent?.({ directory, sessionID }, phase);
47
60
  return true;
@@ -346,7 +346,9 @@ export const createNotificationTriggerRuntime = (deps) => {
346
346
 
347
347
  const extractSessionIdFromPayload = (payload) => {
348
348
  if (!payload || typeof payload !== 'object') return null;
349
- const props = payload.properties;
349
+ const props = payload.properties && typeof payload.properties === 'object'
350
+ ? payload.properties
351
+ : (payload.data && typeof payload.data === 'object' ? payload.data : null);
350
352
  const info = props?.info;
351
353
  const sessionId =
352
354
  info?.sessionID ??
@@ -412,20 +414,36 @@ export const createNotificationTriggerRuntime = (deps) => {
412
414
  }
413
415
  }
414
416
  }
415
- if ((payload.type === 'session.idle' || payload.type === 'session.error') && sessionId) {
416
- const error = payload.properties?.error;
417
+ // Ticket 07: shutdown interrupt is not a completion terminal — no ready push.
418
+ if (payload.type === 'session.execution.interrupted') {
419
+ const reason = payload.properties?.reason ?? payload.data?.reason;
420
+ if (reason === 'shutdown') return;
421
+ }
422
+ // v2 completion is session.execution.succeeded. session.idle is a deprecated
423
+ // fallback; the ready-notification cooldown below collapses the pair.
424
+ if ((
425
+ payload.type === 'session.idle'
426
+ || payload.type === 'session.execution.succeeded'
427
+ || payload.type === 'session.error'
428
+ || payload.type === 'session.execution.failed'
429
+ ) && sessionId) {
430
+ const body = payload.properties && typeof payload.properties === 'object'
431
+ ? payload.properties
432
+ : (payload.data && typeof payload.data === 'object' ? payload.data : {});
433
+ const error = body?.error;
417
434
  const errorText = typeof error?.message === 'string'
418
435
  ? error.message
419
436
  : typeof error === 'string' ? error : '';
437
+ const failed = payload.type === 'session.error' || payload.type === 'session.execution.failed';
420
438
  await maybeSendPushForTrigger({
421
439
  ...payload,
422
440
  type: 'message.updated',
423
441
  properties: {
424
- ...payload.properties,
442
+ ...(payload.properties && typeof payload.properties === 'object' ? payload.properties : body),
425
443
  info: {
426
444
  sessionID: sessionId,
427
445
  role: 'assistant',
428
- finish: payload.type === 'session.error' ? 'error' : 'stop',
446
+ finish: failed ? 'error' : 'stop',
429
447
  ...(errorText ? { parts: [{ type: 'text', text: errorText }] } : {}),
430
448
  },
431
449
  },
@@ -457,6 +457,17 @@ describe('contact turn notifications', () => {
457
457
  });
458
458
 
459
459
  describe('notification trigger live activity end', () => {
460
+ it('treats session.execution.succeeded as completion, including a v2 data envelope', async () => {
461
+ vi.stubGlobal('fetch', vi.fn(async () => rootSessionResponse('ses_done')));
462
+ const { runtime, sendLiveActivityEnd, emitDesktopNotification } = createRuntime();
463
+ await runtime.maybeSendPushForTrigger({
464
+ type: 'session.execution.succeeded',
465
+ data: { sessionID: 'ses_done' },
466
+ });
467
+ expect(sendLiveActivityEnd).toHaveBeenCalledWith({ sessionId: 'ses_done', status: 'complete' });
468
+ expect(emitDesktopNotification).toHaveBeenCalled();
469
+ });
470
+
460
471
  it('ends the live activity on top-level completion and error', async () => {
461
472
  vi.stubGlobal('fetch', vi.fn(async () => rootSessionResponse()));
462
473
  const { runtime, sendLiveActivityEnd } = createRuntime();