@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
@@ -33,7 +33,7 @@ The service calls `waitForOpenCodeReady(6000, 75)` (6s, matching proxy gate `REA
33
33
  - `packages/web/server/lib/conversations/validation.js`
34
34
  - Request validation and sanitization with strict whitelist
35
35
  - Required: `input.type='prompt'`, `directory`, `messageID`, `model` (providerID/modelID), `parts` (non-empty array)
36
- - Optional: `title`, `parentID`, `agent`, `variant`, `metadata`
36
+ - Optional: `title`, `parentID`, `agent`, `variant`, `metadata`, `skills` (array of `{ id }`, sanitized before forwarding as native V2 skill attachments)
37
37
  - **Rejected**: `delivery`, `format`, unknown top-level keys (whitelist enforcement)
38
38
  - Part types: `text` (requires non-empty `text`), `file` (requires `mime`+`url`), `agent` (requires `name`)
39
39
  - At least one content-carrying part (text/file/agent) required
@@ -55,6 +55,7 @@ const toV2PromptInput = (sanitizedInput, sessionID) => {
55
55
  text,
56
56
  ...(files.length > 0 ? { files } : {}),
57
57
  ...(agents.length > 0 ? { agents } : {}),
58
+ ...(sanitizedInput.skills?.length ? { skills: sanitizedInput.skills.map(({ id }) => ({ id, name: id })) } : {}),
58
59
  ...(sanitizedInput.metadata ? { metadata: sanitizedInput.metadata } : {}),
59
60
  delivery: 'steer',
60
61
  };
@@ -393,6 +393,22 @@ describe('conversations service', () => {
393
393
 
394
394
  // --- success ---
395
395
 
396
+ it('validates and forwards skill ids with the first prompt', async () => {
397
+ const validated = validateConversationInput({
398
+ input: { type: 'prompt' },
399
+ ...baseInput({ parts: [{ type: 'text', text: '[skill:release] prepare notes' }], skills: [{ id: 'release', text: 'untrusted body' }] }),
400
+ });
401
+ expect(validated.valid).toBe(true);
402
+ expect(validated.sanitized.skills).toEqual([{ id: 'release' }]);
403
+ const prompt = vi.fn(async () => undefined);
404
+ mockMake.mockReturnValue({ session: { create: async () => ({ id: 'ses_skill' }), prompt } });
405
+ const result = await createService().createAndPrompt({ sanitizedInput: validated.sanitized });
406
+ expect(result.ok).toBe(true);
407
+ expect(prompt.mock.calls[0][0]).toMatchObject({
408
+ text: '[skill:release] prepare notes', skills: [{ id: 'release', name: 'release' }], delivery: 'steer',
409
+ });
410
+ });
411
+
396
412
  it('returns success when create + prompt succeed', async () => {
397
413
  const sessionData = { id: 'ses_created', title: 'Test' };
398
414
  mockMake.mockReturnValue({
@@ -1,6 +1,6 @@
1
1
  const ALLOWED_TOP_LEVEL_KEYS = new Set([
2
2
  'input', 'directory', 'messageID', 'model', 'parts',
3
- 'title', 'parentID', 'agent', 'variant', 'metadata',
3
+ 'title', 'parentID', 'agent', 'variant', 'metadata', 'skills',
4
4
  ]);
5
5
  const ALLOWED_PART_TYPES = new Set(['text', 'file', 'agent']);
6
6
 
@@ -139,6 +139,15 @@ export const validateConversationInput = (body) => {
139
139
  }
140
140
 
141
141
  // Optional fields
142
+ let skills;
143
+ if (body.skills !== undefined) {
144
+ if (!Array.isArray(body.skills) || body.skills.some((skill) =>
145
+ !skill || typeof skill.id !== 'string' || !skill.id.trim())) {
146
+ errors.push('skills must be an array of non-empty skill ids');
147
+ } else {
148
+ skills = body.skills.map((skill) => ({ id: skill.id }));
149
+ }
150
+ }
142
151
  const title = asOptionalNonEmptyString(body.title);
143
152
  if (title === null) errors.push('title must be a non-empty string if provided');
144
153
 
@@ -168,6 +177,7 @@ export const validateConversationInput = (body) => {
168
177
  modelID: body.model.modelID.trim(),
169
178
  },
170
179
  parts: sanitizedParts,
180
+ ...(skills?.length ? { skills } : {}),
171
181
  title,
172
182
  parentID,
173
183
  agent,
@@ -33,7 +33,7 @@ The following APIs are exported by their owning modules. `event-stream/index.js`
33
33
  ### Reasoning projection helpers (`reasoning-projection.js`)
34
34
  Import from the module file (not the event-stream barrel). Public surface:
35
35
  - `shouldIncludeReasoning` / `readIncludeReasoningQuery` / `readIncludeReasoningFromUrl` / `stripIncludeReasoningParam`
36
- - `projectMessagesPayloadForReasoning` / `projectMessagesPayloadWithoutReasoning` — array / `{ records }` / `{ record }` / `{ parts }` only
36
+ - `projectMessagesPayloadForReasoning` / `projectMessagesPayloadWithoutReasoning` — array / `{ records }` / `{ record }` / `{ parts }`, plus native v2 `{ data }` list/context envelopes and assistant `content[]`. Filtering preserves message identities, cursor, text/tool content and token totals.
37
37
  - `createReasoningOutboundFilter` — per-connection stream filter (version-suffix-aware type match; preserves original `type` on kept events)
38
38
  - `filterSseBlock` / `createSseBlockSplitter` — SSE disabled path (CR-safe chunk boundaries)
39
39
 
@@ -49,6 +49,8 @@ Import from the module file (not the event-stream barrel). Public surface:
49
49
  - `createUpstreamSseReader(...)`: creates a start/stop reader for OpenCode SSE streams. The reader parses SSE blocks, tracks the latest `Last-Event-ID`, reconnects after closed or stalled upstream streams, and reports events through callbacks.
50
50
 
51
51
  ## Runtime behavior
52
+
53
+ Native v2 `session.reasoning.*` events are filtered at the same outbound boundary as legacy `session.next.reasoning.*`. Both `/api/session/:id/message` and `/api/session/:id/context` use the snapshot projection before relay encryption.
52
54
  - Browser clients connect to the WS endpoints above.
53
55
  - OpenChamber still fetches OpenCode upstream event streams over SSE.
54
56
  - The web server creates one shared global message-stream hub. OpenCode watcher side effects and global WS clients subscribe to that hub, so there is one upstream `/global/event` SSE reader for both server-side processing and browser fan-out.
@@ -21,6 +21,8 @@ export function acceptDirectoryMessageStreamWsConnection({
21
21
  buildOpenCodeUrl,
22
22
  getOpenCodeAuthHeaders,
23
23
  processForwardedEventPayload,
24
+ /** Optional Host archive/metadata projection for session lifecycle events. */
25
+ projectOutboundSessionPayload = null,
24
26
  wsClients,
25
27
  triggerHealthCheck,
26
28
  heartbeatIntervalMs,
@@ -78,14 +80,23 @@ export function acceptDirectoryMessageStreamWsConnection({
78
80
  const run = async () => {
79
81
  const forwardEvent = ({ envelope, payload }) => {
80
82
  const directory = requestedDirectory || envelope?.directory || 'global';
83
+ let outbound = payload;
84
+ if (typeof projectOutboundSessionPayload === 'function') {
85
+ try {
86
+ const projected = projectOutboundSessionPayload(payload);
87
+ if (projected != null) outbound = projected;
88
+ } catch (error) {
89
+ console.warn('[event-stream] directory session projection failed:', error?.message ?? error);
90
+ }
91
+ }
81
92
 
82
- sendMessageStreamWsEvent(socket, payload, {
93
+ sendMessageStreamWsEvent(socket, outbound, {
83
94
  directory,
84
95
  eventId: typeof envelope?.eventId === 'string' && envelope.eventId.length > 0 ? envelope.eventId : undefined,
85
96
  reasoningFilter,
86
97
  });
87
98
 
88
- processForwardedEventPayload(payload, (syntheticPayload) => {
99
+ processForwardedEventPayload(outbound, (syntheticPayload) => {
89
100
  sendMessageStreamWsEvent(socket, syntheticPayload, {
90
101
  directory: 'global',
91
102
  reasoningFilter,
@@ -64,6 +64,13 @@ export function createGlobalMessageStreamHub({
64
64
  upstreamReconnectDelayMs,
65
65
  replayLimit = MESSAGE_STREAM_GLOBAL_REPLAY_LIMIT,
66
66
  getRuntimeIdentity = null,
67
+ /**
68
+ * Optional Host authority projection for session.created / session.updated
69
+ * (and GlobalEvent wrap). Applied before fan-out and replay so browser and
70
+ * server subscribers share the same archive view. Must be sync and cheap.
71
+ * @type {((payload: unknown) => unknown) | null}
72
+ */
73
+ projectOutboundSessionPayload = null,
67
74
  }) {
68
75
  const eventSubscribers = new Set();
69
76
  const statusSubscribers = new Set();
@@ -97,12 +104,19 @@ export function createGlobalMessageStreamHub({
97
104
  }
98
105
  };
99
106
 
107
+ let sessionPayloadProjector = typeof projectOutboundSessionPayload === 'function'
108
+ ? projectOutboundSessionPayload
109
+ : null;
110
+
111
+ /**
112
+ * Diff-summarize only. Host archive projection is applied at fan-out and
113
+ * replayAfter time against the *current* committed store — never baked into
114
+ * the replay buffer (stale archive stamps must not stick after Host writes).
115
+ */
100
116
  const normalizeEvent = ({ envelope, payload, connectionContext }) => {
101
117
  const directory =
102
118
  typeof envelope?.directory === 'string' && envelope.directory.length > 0 ? envelope.directory : 'global';
103
119
  const eventId = typeof envelope?.eventId === 'string' && envelope.eventId.length > 0 ? envelope.eventId : undefined;
104
- // Summarize FileDiff bodies before fan-out and replay buffering so reconnect
105
- // replay cannot re-amplify full patch frames into >64KB WS payloads.
106
120
  const outboundPayload = summarizeOutboundEventPayload(payload);
107
121
  return {
108
122
  envelope,
@@ -113,6 +127,19 @@ export function createGlobalMessageStreamHub({
113
127
  };
114
128
  };
115
129
 
130
+ const projectForOutbound = (payload) => {
131
+ if (typeof sessionPayloadProjector !== 'function') return payload;
132
+ try {
133
+ const projected = sessionPayloadProjector(payload);
134
+ // null = Host not ready; suppress this lifecycle frame.
135
+ if (projected == null) return null;
136
+ return projected;
137
+ } catch (error) {
138
+ console.warn('[event-stream] session payload projection failed:', error?.message ?? error);
139
+ return payload;
140
+ }
141
+ };
142
+
116
143
  const start = () => {
117
144
  if (reader) {
118
145
  return;
@@ -161,6 +188,7 @@ export function createGlobalMessageStreamHub({
161
188
  ...event,
162
189
  connectionContext: event?.connectionContext ?? activeConnectionRuntimeIdentity,
163
190
  });
191
+ // Buffer the unprojected payload so reconnect replay uses current Host authority.
164
192
  if (normalized.eventId) {
165
193
  replay.push(normalized);
166
194
  if (replay.length > replayLimit) {
@@ -168,8 +196,14 @@ export function createGlobalMessageStreamHub({
168
196
  }
169
197
  }
170
198
 
199
+ const projectedPayload = projectForOutbound(normalized.payload);
200
+ if (projectedPayload == null) return;
201
+ const outbound = projectedPayload === normalized.payload
202
+ ? normalized
203
+ : { ...normalized, payload: projectedPayload };
204
+
171
205
  for (const subscriber of Array.from(eventSubscribers)) {
172
- notifySubscriber('event', subscriber, normalized);
206
+ notifySubscriber('event', subscriber, outbound);
173
207
  }
174
208
  },
175
209
  onError(error) {
@@ -227,10 +261,21 @@ export function createGlobalMessageStreamHub({
227
261
  }
228
262
 
229
263
  const index = replay.findIndex((entry) => entry.eventId === eventId);
230
- return index === -1 ? [] : replay.slice(index + 1);
264
+ if (index === -1) return [];
265
+ // Re-project with current committed Host authority at replay time.
266
+ const out = [];
267
+ for (const entry of replay.slice(index + 1)) {
268
+ const projectedPayload = projectForOutbound(entry.payload);
269
+ if (projectedPayload == null) continue;
270
+ out.push(projectedPayload === entry.payload ? entry : { ...entry, payload: projectedPayload });
271
+ }
272
+ return out;
231
273
  },
232
274
  setRuntimeIdentityProvider(provider) {
233
275
  runtimeIdentityProvider = typeof provider === 'function' ? provider : null;
234
276
  },
277
+ setSessionPayloadProjector(projector) {
278
+ sessionPayloadProjector = typeof projector === 'function' ? projector : null;
279
+ },
235
280
  };
236
281
  }
@@ -96,6 +96,10 @@ function projectParts(parts) {
96
96
  * @param {unknown} record
97
97
  */
98
98
  function projectRecord(record) {
99
+ if (isPlainObject(record) && record.type === 'assistant' && Array.isArray(record.content)) {
100
+ const content = projectParts(record.content);
101
+ return content === record.content ? record : { ...record, content };
102
+ }
99
103
  if (!isPlainObject(record) || !Array.isArray(record.parts)) return record;
100
104
  const parts = projectParts(record.parts);
101
105
  return parts === record.parts ? record : { ...record, parts };
@@ -126,6 +130,17 @@ export function projectMessagesPayloadWithoutReasoning(payload) {
126
130
  let next = payload;
127
131
  let changed = false;
128
132
 
133
+ if (Array.isArray(payload.data)) {
134
+ const data = projectMessagesPayloadWithoutReasoning(payload.data);
135
+ if (data !== payload.data) {
136
+ next = { ...next, data };
137
+ changed = true;
138
+ }
139
+ }
140
+ if (payload.type === 'assistant' && Array.isArray(payload.content)) {
141
+ return projectRecord(payload);
142
+ }
143
+
129
144
  if (Array.isArray(payload.records)) {
130
145
  const records = projectMessagesPayloadWithoutReasoning(payload.records);
131
146
  if (records !== payload.records) {
@@ -222,7 +237,7 @@ export function createReasoningOutboundFilter() {
222
237
  const type = baseEventType(rawType);
223
238
 
224
239
  // session.next.reasoning.* (incl. versioned …reasoning.delta.1)
225
- if (type.startsWith('session.next.reasoning.')) return null;
240
+ if (type.startsWith('session.next.reasoning.') || type.startsWith('session.reasoning.')) return null;
226
241
 
227
242
  if (type === 'message.part.updated') {
228
243
  const body = eventBodyOf(payload);
@@ -13,6 +13,21 @@ import {
13
13
  } from './reasoning-projection.js';
14
14
 
15
15
  describe('shouldIncludeReasoning / query helpers', () => {
16
+ it('projects v2 list/context and exact snapshots without dropping rows or cursor', () => {
17
+ const row = { id: 'm', type: 'assistant', tokens: { reasoning: 12 }, content: [
18
+ { type: 'reasoning', text: 'hidden' }, { type: 'text', text: 'visible' },
19
+ ] };
20
+ const payload = { data: [row], cursor: { next: 'older' } };
21
+ const projected = projectMessagesPayloadForReasoning(payload, false);
22
+ expect(projected).toEqual({ data: [{ ...row, content: [row.content[1]] }], cursor: payload.cursor });
23
+ expect(projectMessagesPayloadForReasoning(row, false)).toEqual(projected.data[0]);
24
+ expect(projectMessagesPayloadForReasoning(payload, true)).toBe(payload);
25
+ expect(row.content).toHaveLength(2);
26
+ const filter = createReasoningOutboundFilter();
27
+ expect(filter.projectEvent({ type: 'session.reasoning.delta.1', data: { delta: 'hidden' } })).toBeNull();
28
+ const text = { type: 'session.text.delta', data: { delta: 'visible' } };
29
+ expect(filter.projectEvent(text)).toBe(text);
30
+ });
16
31
  it('only treats the strict string false as disabled', () => {
17
32
  expect(shouldIncludeReasoning(undefined)).toBe(true);
18
33
  expect(shouldIncludeReasoning(null)).toBe(true);
@@ -6,6 +6,7 @@ import {
6
6
  MESSAGE_STREAM_GLOBAL_WS_PATH,
7
7
  MESSAGE_STREAM_WS_HEARTBEAT_INTERVAL_MS,
8
8
  sendMessageStreamWsEvent,
9
+ sendMessageStreamWsFrame,
9
10
  } from './protocol.js';
10
11
  import { createGlobalMessageStreamHub } from './global-hub.js';
11
12
  import { createGlobalMessageStreamWsBridge } from './global-ws-bridge.js';
@@ -62,6 +63,9 @@ export function createMessageStreamWsRuntime({
62
63
  buildOpenCodeUrl,
63
64
  getOpenCodeAuthHeaders,
64
65
  processForwardedEventPayload,
66
+ projectOutboundSessionPayload = null,
67
+ /** Await Host metadata ready before directory WS attaches upstream. */
68
+ ensureSessionMetadataReady = null,
65
69
  wsClients,
66
70
  triggerHealthCheck,
67
71
  heartbeatIntervalMs = MESSAGE_STREAM_WS_HEARTBEAT_INTERVAL_MS,
@@ -104,29 +108,62 @@ export function createMessageStreamWsRuntime({
104
108
  const includeReasoning = shouldIncludeReasoning(requestUrl.searchParams.get('includeReasoning'));
105
109
  const reasoningFilter = includeReasoning ? null : createReasoningOutboundFilter();
106
110
 
107
- if (isGlobalStream) {
108
- globalBridge.accept(socket, {
111
+ // Host metadata readiness gates *both* global and directory streams before
112
+ // accept/hub.start — otherwise globalBridge.accept → hub.start bypasses the
113
+ // gate and lifecycle frames can fan out while the store is still unknown.
114
+ void (async () => {
115
+ if (typeof ensureSessionMetadataReady === 'function') {
116
+ try {
117
+ const ready = await ensureSessionMetadataReady();
118
+ if (!ready) {
119
+ sendMessageStreamWsFrame(socket, {
120
+ type: 'error',
121
+ message: 'session metadata is unavailable',
122
+ code: 'session_metadata_unavailable',
123
+ retryable: true,
124
+ });
125
+ socket.close(1013, 'session metadata unavailable');
126
+ return;
127
+ }
128
+ } catch {
129
+ sendMessageStreamWsFrame(socket, {
130
+ type: 'error',
131
+ message: 'session metadata is unavailable',
132
+ code: 'session_metadata_unavailable',
133
+ retryable: true,
134
+ });
135
+ socket.close(1013, 'session metadata unavailable');
136
+ return;
137
+ }
138
+ }
139
+
140
+ if (socket.readyState !== 1) return;
141
+
142
+ if (isGlobalStream) {
143
+ globalBridge.accept(socket, {
144
+ requestedLastEventId,
145
+ reasoningFilter,
146
+ });
147
+ return;
148
+ }
149
+
150
+ acceptDirectoryMessageStreamWsConnection({
151
+ socket,
109
152
  requestedLastEventId,
153
+ requestedDirectory,
110
154
  reasoningFilter,
155
+ buildOpenCodeUrl,
156
+ getOpenCodeAuthHeaders,
157
+ processForwardedEventPayload,
158
+ projectOutboundSessionPayload,
159
+ wsClients,
160
+ triggerHealthCheck,
161
+ heartbeatIntervalMs,
162
+ upstreamStallTimeoutMs,
163
+ upstreamReconnectDelayMs,
164
+ fetchImpl,
111
165
  });
112
- return;
113
- }
114
-
115
- acceptDirectoryMessageStreamWsConnection({
116
- socket,
117
- requestedLastEventId,
118
- requestedDirectory,
119
- reasoningFilter,
120
- buildOpenCodeUrl,
121
- getOpenCodeAuthHeaders,
122
- processForwardedEventPayload,
123
- wsClients,
124
- triggerHealthCheck,
125
- heartbeatIntervalMs,
126
- upstreamStallTimeoutMs,
127
- upstreamReconnectDelayMs,
128
- fetchImpl,
129
- });
166
+ })();
130
167
  });
131
168
 
132
169
  const upgradeHandler = (req, socket, head) => {
@@ -126,6 +126,118 @@ describe('event stream broadcaster', () => {
126
126
  });
127
127
  });
128
128
 
129
+ describe('message stream websocket runtime Host metadata readiness', () => {
130
+ it('does not accept/start global hub while metadata load is pending', async () => {
131
+ const server = new EventEmitter();
132
+ const wsClients = new Set();
133
+ let fetchCalls = 0;
134
+ let releaseReady;
135
+ const readyGate = new Promise((resolve) => { releaseReady = resolve; });
136
+
137
+ const runtime = createMessageStreamWsRuntime({
138
+ server,
139
+ uiAuthController: null,
140
+ isRequestOriginAllowed: async () => true,
141
+ rejectWebSocketUpgrade() {},
142
+ buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
143
+ getOpenCodeAuthHeaders: () => ({}),
144
+ processForwardedEventPayload() {},
145
+ ensureSessionMetadataReady: async () => {
146
+ await readyGate;
147
+ return true;
148
+ },
149
+ wsClients,
150
+ upstreamReconnectDelayMs: 0,
151
+ fetchImpl: async (_url, options) => {
152
+ fetchCalls += 1;
153
+ return createSseResponse({
154
+ signal: options.signal,
155
+ holdOpen: true,
156
+ blocks: [
157
+ 'id: evt-1\ndata: {"type":"session.updated","properties":{"info":{"id":"ses_1","time":{}}}}\n\n',
158
+ ],
159
+ });
160
+ },
161
+ });
162
+
163
+ const socket = new FakeSocket();
164
+ runtime.wsServer.emit('connection', socket, { url: '/api/global/event/ws' });
165
+ await new Promise((resolve) => setTimeout(resolve, 15));
166
+
167
+ // Still pending → no hub.start → no upstream fetch, no accept frames yet.
168
+ expect(fetchCalls).toBe(0);
169
+ expect(socket.closeCalls).toEqual([]);
170
+ expect(socket.sent.some((f) => f.type === 'event')).toBe(false);
171
+
172
+ releaseReady(true);
173
+ await new Promise((resolve) => setTimeout(resolve, 30));
174
+
175
+ expect(fetchCalls).toBe(1);
176
+ expect(socket.sent.some((f) => f.type === 'event'
177
+ && f.payload?.type === 'session.updated')).toBe(true);
178
+
179
+ socket.close();
180
+ await runtime.close();
181
+ });
182
+
183
+ it('rejects global connection when metadata ready fails', async () => {
184
+ const server = new EventEmitter();
185
+ const runtime = createMessageStreamWsRuntime({
186
+ server,
187
+ uiAuthController: null,
188
+ isRequestOriginAllowed: async () => true,
189
+ rejectWebSocketUpgrade() {},
190
+ buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
191
+ getOpenCodeAuthHeaders: () => ({}),
192
+ processForwardedEventPayload() {},
193
+ ensureSessionMetadataReady: async () => false,
194
+ wsClients: new Set(),
195
+ upstreamReconnectDelayMs: 0,
196
+ fetchImpl: async () => {
197
+ throw new Error('upstream must not start');
198
+ },
199
+ });
200
+
201
+ const socket = new FakeSocket();
202
+ runtime.wsServer.emit('connection', socket, { url: '/api/global/event/ws' });
203
+ await new Promise((resolve) => setTimeout(resolve, 15));
204
+
205
+ expect(socket.sent.some((f) => f.type === 'error'
206
+ && f.code === 'session_metadata_unavailable'
207
+ && f.retryable === true)).toBe(true);
208
+ expect(socket.closeCalls.some((c) => c.code === 1013)).toBe(true);
209
+
210
+ await runtime.close();
211
+ });
212
+
213
+ it('gates directory stream the same way as global', async () => {
214
+ const server = new EventEmitter();
215
+ const runtime = createMessageStreamWsRuntime({
216
+ server,
217
+ uiAuthController: null,
218
+ isRequestOriginAllowed: async () => true,
219
+ rejectWebSocketUpgrade() {},
220
+ buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
221
+ getOpenCodeAuthHeaders: () => ({}),
222
+ processForwardedEventPayload() {},
223
+ ensureSessionMetadataReady: async () => false,
224
+ wsClients: new Set(),
225
+ upstreamReconnectDelayMs: 0,
226
+ fetchImpl: async () => {
227
+ throw new Error('upstream must not start');
228
+ },
229
+ });
230
+
231
+ const socket = new FakeSocket();
232
+ runtime.wsServer.emit('connection', socket, { url: '/api/event/ws?directory=/repo' });
233
+ await new Promise((resolve) => setTimeout(resolve, 15));
234
+
235
+ expect(socket.sent.some((f) => f.code === 'session_metadata_unavailable')).toBe(true);
236
+ expect(socket.closeCalls.some((c) => c.code === 1013)).toBe(true);
237
+ await runtime.close();
238
+ });
239
+ });
240
+
129
241
  describe('message stream websocket runtime', () => {
130
242
  it('filters reasoning events per connection when includeReasoning=false', async () => {
131
243
  const server = new EventEmitter();
@@ -23,8 +23,8 @@ only model path the Assistant harness (`pi-agent-core` `streamFn`) talks to.
23
23
 
24
24
  `variant` is an optional OpenCode model variant string. Null, omitted, or empty
25
25
  values preserve the provider default. Other types fail validation before a model
26
- call. A selected variant is forwarded unchanged on `generate.text` and on the
27
- attachment-session `model` ref. OpenCode owns provider-specific variant
26
+ call. A selected variant is forwarded unchanged on the throwaway session's
27
+ `model` ref (both paths). OpenCode owns provider-specific variant
28
28
  semantics. Variant selection changes inference configuration only; completion
29
29
  output continues to include text parts and keeps reasoning parts private.
30
30
 
@@ -35,10 +35,26 @@ call Anthropic/OpenAI/plugin SDKs, and does not use the `openai` npm package.
35
35
 
36
36
  ### Text path (no vision image bytes)
37
37
 
38
- `client.generate.text({ location?, prompt, model: { id, providerID, variant? } },
39
- { signal })` → `{ text }`. No session. System text is prepended into `prompt`.
38
+ `session.generate` inside a throwaway session owned by the hidden deny-all
39
+ `openchamber-text` agent (same temp root, same deny-all verification and
40
+ `session.create` / `session.remove` lifecycle as the attachment path below):
41
+
42
+ 1. Ensure `.opencode/agent/openchamber-text.md` and verify its final
43
+ permission rule is deny-all before creating the session.
44
+ 2. `session.create({ location, agent: 'openchamber-text', model, metadata })`.
45
+ 3. `session.generate({ sessionID, prompt })` → `{ text }`. One model call with
46
+ the session's provider context, no tool loop, no messages written. System
47
+ text is prepended into `prompt`.
48
+ 4. Always `session.remove` in `finally` (interrupt first on abort/timeout).
49
+
50
+ `generate.text` (sessionless) is not used: session-scoped providers such as
51
+ OpenCode Go reject calls without a session (`x-opencode-session`).
40
52
  `onTextDelta` is skipped honestly (no fake typewriter).
41
53
 
54
+ OpenCode loads a fresh location's config lazily, so `agent.get` can report the
55
+ agent as missing on the first read. Deny-all verification polls within a
56
+ bounded window (5s) before failing.
57
+
42
58
  ### Attachment / vision path
43
59
 
44
60
  When the catalog marks the model vision-capable and the request includes image
@@ -49,8 +65,11 @@ data-URL file parts:
49
65
  2. `agent.get({ agentID: 'openchamber-llm', location })` and require the **final**
50
66
  permissions rule to be deny-all. Failure → `llm_attachment_generation_unavailable`
51
67
  **before** any `session.prompt`. Title is never treated as permission isolation.
52
- 3. `session.create({ location, agent, model: { id, providerID, variant? } })`
53
- (bare session id).
68
+ 3. `session.create({ location, agent, model: { id, providerID, variant? },
69
+ metadata: { openchamber: { llm: { purpose: 'chat-completions' } } } })`
70
+ (bare session id). Host `persistSessionMetadata` then commits the same
71
+ isolation patch; `onSystemSessionPersisted` drops the throwaway session
72
+ from the sidebar index. Create-time `metadata` is a hint only.
54
73
  4. Optional `session.instructions.entry.put` for the system prompt.
55
74
  5. Subscribe for `session.text.delta` (`data.sessionID` / `assistantMessageID` /
56
75
  `ordinal` / `delta`) before prompt; unsubscribe in `finally`.
@@ -81,7 +100,7 @@ There is no `config.providers` catalog path.
81
100
  `onTextDelta(text: string)` and `globalEventHub`.
82
101
 
83
102
  - **Attachment session path:** real `session.text.delta` tokens for this session.
84
- - **`generate.text` path:** cannot emit live deltas — skip `onTextDelta`.
103
+ - **`session.generate` text path:** cannot emit live deltas — skip `onTextDelta`.
85
104
  - **HTTP `POST .../chat/completions`:** does not pass these options and still
86
105
  rejects `stream: true`.
87
106
 
@@ -99,8 +118,13 @@ application tool catalog and actual results.
99
118
  The Assistant contact harness owns system prompt, OpenChamber transcript,
100
119
  bubble splitting, and OpenChamber API tools (`assign_session`). Those tools
101
120
  deliver through contact **cards**, not this completions payload. The gateway
102
- stays a text generator: OpenCode coding tools stay denied on the attachment path
103
- via the verified agent permissions ruleset.
121
+ stays a text generator: OpenCode coding tools stay denied on both paths via the
122
+ verified agent permissions ruleset.
123
+
124
+ The gateway is also the single OpenCode-backed path for server utility calls
125
+ (`small-model`: commit messages, session titles, goal audits, scheduled-task
126
+ distill, TTS summaries, PR descriptions). No server module calls provider APIs
127
+ directly with OpenCode credentials.
104
128
 
105
129
  Internal completion/generate calls accept an optional AbortSignal from the
106
130
  contact continuation. It is combined with the generator deadline and passed to
@@ -57,7 +57,7 @@ export function projectConnectedModels(source) {
57
57
  for (const model of modelsIn) {
58
58
  if (!isRecord(model)) continue;
59
59
  const providerID = stringID(model.providerID);
60
- // External model id for generate.text / session model refs.
60
+ // External model id for session model refs.
61
61
  const modelID = stringID(model.id);
62
62
  if (!providerID || !modelID) continue;
63
63
  if (providerMeta.size > 0 && !providerMeta.has(providerID)) continue;
@@ -96,11 +96,13 @@ export async function createChatCompletion({
96
96
  ensureTempDirectory,
97
97
  onTextDelta = null,
98
98
  globalEventHub = null,
99
+ persistSessionMetadata = null,
100
+ onSystemSessionPersisted = null,
99
101
  signal = null,
100
102
  }) {
101
103
  signal?.throwIfAborted();
102
104
  if (!isRecord(body)) throw new LlmError('validation_error', 400, 'JSON body is required');
103
- // generate.text / attachment-session return a full turn. This HTTP gateway
105
+ // session.generate / attachment-session return a full turn. This HTTP gateway
104
106
  // does not token-stream. Do not emit fake SSE after the fact.
105
107
  // In-process onTextDelta (optional) is a separate internal callback path.
106
108
  if (body.stream === true) {
@@ -148,6 +150,8 @@ export async function createChatCompletion({
148
150
  ))?.acceptsImages),
149
151
  onTextDelta,
150
152
  globalEventHub,
153
+ persistSessionMetadata,
154
+ onSystemSessionPersisted,
151
155
  signal,
152
156
  });
153
157
  } catch (error) {
@@ -52,7 +52,7 @@ describe('createChatCompletion', () => {
52
52
  })
53
53
 
54
54
  it('returns a non-stream completion from generateText', async () => {
55
- const generateText = vi.fn(async () => ({ text: 'done', source: 'generate.text' }))
55
+ const generateText = vi.fn(async () => ({ text: 'done', source: 'session.generate' }))
56
56
  const result = await createChatCompletion({
57
57
  generateText,
58
58
  loadCatalog: async () => ({
@@ -64,7 +64,7 @@ describe('createChatCompletion', () => {
64
64
  expect(result.completion.choices[0].message.content).toBe('done')
65
65
  expect(result.providerID).toBe('openai')
66
66
  expect(result.modelID).toBe('gpt-5.2')
67
- expect(result.source).toBe('generate.text')
67
+ expect(result.source).toBe('session.generate')
68
68
  })
69
69
 
70
70
  it('forwards in-process onTextDelta and globalEventHub to generateText', async () => {
@@ -109,7 +109,7 @@ describe('createChatCompletion', () => {
109
109
  })
110
110
 
111
111
  it('asks generate to skip image bytes when the catalog model is not vision-capable', async () => {
112
- const generateText = vi.fn(async () => ({ text: 'ok', source: 'generate.text' }))
112
+ const generateText = vi.fn(async () => ({ text: 'ok', source: 'session.generate' }))
113
113
  await createChatCompletion({
114
114
  generateText,
115
115
  loadCatalog: async () => ({