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

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 (279) 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-B_JQoreg.js} +1 -1
  7. package/dist/assets/AssistantView-ClQAfGOT.js +3 -0
  8. package/dist/assets/{DiagramView-xuSJTnv7.js → DiagramView-BNnda8qO.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-Ov0D96d0.js → FadeInOnReveal-VYQTSAkr.js} +1 -1
  10. package/dist/assets/{Icon-DD-BuMfZ.js → Icon-BiEQEcih.js} +1 -1
  11. package/dist/assets/{MarkdownRenderer-bc_p1H7l.js → MarkdownRenderer-CJHSsWin.js} +2 -2
  12. package/dist/assets/{MarkdownRendererImpl-SKi4oG0c.js → MarkdownRendererImpl-DwrHFGUp.js} +3 -3
  13. package/dist/assets/MarkstreamRendererImpl-CSnbQMu3.js +3 -0
  14. package/dist/assets/{MobileDetailNavigation-Bnx59U_J.js → MobileDetailNavigation-BXr6AGLT.js} +1 -1
  15. package/dist/assets/{MobileShareBridge-BQlP4J-F.js → MobileShareBridge-D7OE9OP_.js} +2 -2
  16. package/dist/assets/MobileSurface-D2jUkX9l.js +2 -0
  17. package/dist/assets/{MultiRunWindow-ChZGob0v.js → MultiRunWindow-DF3iIFCd.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-DtQJxZG-.js → OnboardingScreen-BjHmlwlw.js} +1 -1
  19. package/dist/assets/OverlayScrollbar-DLtWO_Gp.js +2 -0
  20. package/dist/assets/SettingsView-BRxELjow.js +25 -0
  21. package/dist/assets/{SettingsWindow-DfnsFa8y.js → SettingsWindow-BI5swspb.js} +1 -1
  22. package/dist/assets/{TerminalView-Dh9MWCpS.js → TerminalView-BU3x8-fr.js} +1 -1
  23. package/dist/assets/{ToolOutputDialog-BejmjrRk.js → ToolOutputDialog-BEWhBcRd.js} +2 -2
  24. package/dist/assets/appThemeRegistry-4MR0F7Jz.js +1 -0
  25. package/dist/assets/{button-D-rATN7w.js → button-DKuz3L8l.js} +1 -1
  26. package/dist/assets/collapsible-DXbDynN4.js +3 -0
  27. package/dist/assets/context-menu-CqolsLCm.js +1 -0
  28. package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-CoDkOnyS.js} +1 -1
  29. package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DDlVKyOE.js} +1 -1
  30. package/dist/assets/{diagram-DQf7Y_uR.js → diagram-D7T8HQgj.js} +1 -1
  31. package/dist/assets/{dialog-bfbvR4VH.js → dialog-DQYl3W_o.js} +1 -1
  32. package/dist/assets/es-CN3E_zXj.js +15 -0
  33. package/dist/assets/{fileMentionTouchSelection-BvrNCroG.js → fileMentionTouchSelection-BZ6ovBYH.js} +1 -1
  34. package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-JrpcsOCs.js} +1 -1
  35. package/dist/assets/fr-9zZvQZD2.js +13 -0
  36. package/dist/assets/gitApi-CM42qUed.js +10 -0
  37. package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-CIl0FL10.js} +1 -1
  38. package/dist/assets/{input-8r6F_Fza.js → input-CzQy8UhV.js} +1 -1
  39. package/dist/assets/insertionBoundaries-CtkZ9720.js +1 -0
  40. package/dist/assets/ja-Dnjk_82N.js +15 -0
  41. package/dist/assets/ko-BxCEE99C.js +15 -0
  42. package/dist/assets/main-BILgBI3B.js +169 -0
  43. package/dist/assets/{main-CCE_4y5F.js → main-BeveiW6F.js} +2 -2
  44. package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
  45. package/dist/assets/{markdownHeightCache-1Qo6y6IJ.js → markdownHeightCache-Cj1I5NDw.js} +1 -1
  46. package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
  47. package/dist/assets/mcpQueries-D5mQymgM.js +1 -0
  48. package/dist/assets/miniChat-BgrzM3u7.js +2 -0
  49. package/dist/assets/mobile-DtYoQE8C.js +2 -0
  50. package/dist/assets/multirun-jy8dEI96.js +4 -0
  51. package/dist/assets/{persistence-Ce3AnpEr.js → persistence-C9LQpiOS.js} +2 -2
  52. package/dist/assets/pl-CNNsWnPM.js +15 -0
  53. package/dist/assets/platform-BQghHRfp.js +6 -0
  54. package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
  55. package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +2 -0
  56. package/dist/assets/renderMobileApp-u3c4GtFC.js +28 -0
  57. package/dist/assets/{runtimeConfig-BMvqa5we.js → runtimeConfig-BIGBRGQI.js} +1 -1
  58. package/dist/assets/runtimeEndpointReset-D2savkuw.js +147 -0
  59. package/dist/assets/sessionChangeSummary-eIbpSfrO.js +2 -0
  60. package/dist/assets/{sessionLookup-gh-hVRIc.js → sessionLookup-myHe5ltu.js} +1 -1
  61. package/dist/assets/{sortable-tabs-strip-CudoSgV4.js → sortable-tabs-strip-BQtmHx9r.js} +1 -1
  62. package/dist/assets/src-CLN7Z-zx.css +2 -0
  63. package/dist/assets/store-DLCiZaA6.js +15 -0
  64. package/dist/assets/{terminalApi-DiF6JmBP.js → terminalApi-BxcdiduX.js} +1 -1
  65. package/dist/assets/textarea-CakKvqFw.js +1 -0
  66. package/dist/assets/{tooltip-CQvpK-UV.js → tooltip-Cx5gTbdY.js} +1 -1
  67. package/dist/assets/uk-rwQrxeYM.js +15 -0
  68. package/dist/assets/{url-_z7evzdK.js → url-CDHnjlsm.js} +1 -1
  69. package/dist/assets/{useAppFontEffects-Cpmpo7Wb.js → useAppFontEffects-CNeo0aZs.js} +59 -45
  70. package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
  71. package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +1 -0
  72. package/dist/assets/useEffectiveDirectory-B4LvR8Gt.js +1 -0
  73. package/dist/assets/{useFontPreferences-DcbXQOkc.js → useFontPreferences-CUnQ4TVm.js} +1 -1
  74. package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +414 -0
  75. package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
  76. package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +2 -0
  77. package/dist/assets/{useThemeSystem-DMzWVJaW.js → useThemeSystem-BlNUWH47.js} +1 -1
  78. package/dist/assets/useUIStore-CQYKJnrg.js +1 -0
  79. package/dist/assets/{useWorkerHighlightedLines-Bqn4AiBL.js → useWorkerHighlightedLines-CJZbJDW3.js} +2 -2
  80. package/dist/assets/{vendor-base-ui-CnHfUmDN.js → vendor-base-ui-CDjyofeD.js} +1 -1
  81. package/dist/assets/{vendor-cmdk-CeAGfrLo.js → vendor-cmdk-BFH0UpXX.js} +1 -1
  82. package/dist/assets/{vendor-dnd-kit-accessibility-BY5LhhUB.js → vendor-dnd-kit-accessibility-BrR3AKvM.js} +1 -1
  83. package/dist/assets/{vendor-dnd-kit-core-CvWxUPdb.js → vendor-dnd-kit-core-CNfdvk6H.js} +1 -1
  84. package/dist/assets/{vendor-dnd-kit-sortable-hHdP9lLb.js → vendor-dnd-kit-sortable-DQUaTeGj.js} +1 -1
  85. package/dist/assets/{vendor-framer-motion-DFy_BSVd.js → vendor-framer-motion-4dogsUk3.js} +1 -1
  86. package/dist/assets/{vendor-legendapp-list-WE32lwNk.js → vendor-legendapp-list-BU7MdPZR.js} +1 -1
  87. package/dist/assets/{vendor-markstream-react-DX55XLGO.js → vendor-markstream-react-qHC2FykU.js} +3 -3
  88. package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
  89. package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
  90. package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
  91. package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
  92. package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
  93. package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
  94. package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
  95. package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
  96. package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
  97. package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
  98. package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
  99. package/dist/index.html +12 -12
  100. package/dist/mini-chat.html +12 -12
  101. package/dist/mobile.html +12 -12
  102. package/package.json +1 -1
  103. package/server/index.js +327 -62
  104. package/server/lib/assistants/DOCUMENTATION.md +14 -9
  105. package/server/lib/assistants/contracts.js +11 -1
  106. package/server/lib/assistants/projection-history.test.js +265 -18
  107. package/server/lib/assistants/routes.js +6 -0
  108. package/server/lib/assistants/service.js +417 -231
  109. package/server/lib/assistants/service.test.js +392 -148
  110. package/server/lib/conversations/DOCUMENTATION.md +1 -1
  111. package/server/lib/conversations/service.js +1 -0
  112. package/server/lib/conversations/service.test.js +16 -0
  113. package/server/lib/conversations/validation.js +11 -1
  114. package/server/lib/event-stream/directory-ws-bridge.js +13 -2
  115. package/server/lib/event-stream/global-hub.js +49 -4
  116. package/server/lib/event-stream/runtime.js +57 -20
  117. package/server/lib/event-stream/runtime.test.js +112 -0
  118. package/server/lib/llm/DOCUMENTATION.md +33 -9
  119. package/server/lib/llm/catalog.js +1 -1
  120. package/server/lib/llm/completions.js +5 -1
  121. package/server/lib/llm/completions.test.js +3 -3
  122. package/server/lib/llm/generate.js +237 -145
  123. package/server/lib/llm/generate.test.js +201 -46
  124. package/server/lib/llm/routes.js +2 -0
  125. package/server/lib/message-queue/runtime.js +15 -2
  126. package/server/lib/notifications/runtime.js +5 -0
  127. package/server/lib/opencode/DOCUMENTATION.md +81 -17
  128. package/server/lib/opencode/agent-document.d.ts +28 -0
  129. package/server/lib/opencode/agent-document.js +280 -0
  130. package/server/lib/opencode/agent-document.test.js +137 -0
  131. package/server/lib/opencode/agents.js +96 -0
  132. package/server/lib/opencode/auth-state-runtime.js +5 -0
  133. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  134. package/server/lib/opencode/config-entity-routes.js +36 -98
  135. package/server/lib/opencode/config-entity-routes.test.js +82 -0
  136. package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
  137. package/server/lib/opencode/ensure-cli.js +1 -2
  138. package/server/lib/opencode/env-runtime.js +40 -26
  139. package/server/lib/opencode/env-runtime.test.js +10 -17
  140. package/server/lib/opencode/feature-routes-runtime.js +43 -4
  141. package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
  142. package/server/lib/opencode/http-server-connections.js +98 -0
  143. package/server/lib/opencode/http-server-connections.test.js +321 -0
  144. package/server/lib/opencode/lifecycle.js +349 -39
  145. package/server/lib/opencode/lifecycle.test.js +322 -0
  146. package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
  147. package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
  148. package/server/lib/opencode/openchamber-routes.js +7 -1
  149. package/server/lib/opencode/opencode2-pin.js +4 -4
  150. package/server/lib/opencode/opencode2-pin.test.js +1 -0
  151. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +127 -4
  152. package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
  153. package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
  154. package/server/lib/opencode/plugin-routes.js +3 -18
  155. package/server/lib/opencode/plugin-routes.test.js +9 -9
  156. package/server/lib/opencode/provider-catalog.js +58 -12
  157. package/server/lib/opencode/provider-catalog.test.js +48 -0
  158. package/server/lib/opencode/proxy.js +249 -46
  159. package/server/lib/opencode/proxy.test.js +220 -2
  160. package/server/lib/opencode/routes.js +296 -62
  161. package/server/lib/opencode/routes.test.js +278 -12
  162. package/server/lib/opencode/runtime-contract.js +401 -0
  163. package/server/lib/opencode/runtime-contract.test.js +214 -0
  164. package/server/lib/opencode/server-opencode-fetch.js +242 -0
  165. package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
  166. package/server/lib/opencode/server-utils-runtime.js +33 -0
  167. package/server/lib/opencode/shared-service.integration.test.js +66 -0
  168. package/server/lib/opencode/shared-service.js +147 -0
  169. package/server/lib/opencode/shared-service.test.js +93 -0
  170. package/server/lib/opencode/shared.js +3 -3
  171. package/server/lib/opencode/shutdown-runtime.js +15 -6
  172. package/server/lib/opencode/shutdown-runtime.test.js +49 -1
  173. package/server/lib/opencode/skill-routes.js +20 -33
  174. package/server/lib/opencode/skill-routes.test.js +35 -2
  175. package/server/lib/opencode/skills.test.js +15 -0
  176. package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
  177. package/server/lib/opencode/static-routes-runtime.js +3 -1
  178. package/server/lib/opencode/static-routes-runtime.test.js +33 -0
  179. package/server/lib/opencode/v2-client.js +14 -3
  180. package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
  181. package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
  182. package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
  183. package/server/lib/permission-auto-accept/runtime.js +14 -7
  184. package/server/lib/permission-auto-accept/runtime.test.js +27 -12
  185. package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
  186. package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -0
  187. package/server/lib/scheduled-tasks/runtime.js +52 -4
  188. package/server/lib/scheduled-tasks/runtime.test.js +80 -1
  189. package/server/lib/session-goal/DOCUMENTATION.md +113 -13
  190. package/server/lib/session-goal/routes.js +9 -0
  191. package/server/lib/session-goal/runtime.js +1579 -117
  192. package/server/lib/session-goal/runtime.test.js +1453 -84
  193. package/server/lib/session-index/DOCUMENTATION.md +18 -15
  194. package/server/lib/session-index/event-ingest.js +89 -16
  195. package/server/lib/session-index/event-ingest.test.js +73 -0
  196. package/server/lib/session-index/host-authority-snapshot.js +92 -0
  197. package/server/lib/session-index/routes.host-archive.test.js +201 -0
  198. package/server/lib/session-index/routes.js +63 -10
  199. package/server/lib/session-index/routes.test.js +8 -8
  200. package/server/lib/session-index/service.js +7 -1
  201. package/server/lib/session-index/sync-runtime.js +161 -34
  202. package/server/lib/session-index/sync-runtime.test.js +155 -1
  203. package/server/lib/session-metadata/DOCUMENTATION.md +61 -16
  204. package/server/lib/session-metadata/routes.js +108 -9
  205. package/server/lib/session-metadata/routes.test.js +47 -2
  206. package/server/lib/session-metadata/session-archive.d.ts +73 -0
  207. package/server/lib/session-metadata/session-archive.js +654 -0
  208. package/server/lib/session-metadata/session-archive.test.js +359 -0
  209. package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
  210. package/server/lib/session-metadata/session-metadata-store.js +212 -72
  211. package/server/lib/session-metadata/session-metadata-store.test.js +205 -10
  212. package/server/lib/session-metadata/session-projection.d.ts +41 -0
  213. package/server/lib/session-metadata/session-projection.js +255 -0
  214. package/server/lib/session-metadata/session-projection.test.js +64 -0
  215. package/server/lib/session-metadata/system-session.js +55 -0
  216. package/server/lib/session-metadata/system-session.test.js +55 -0
  217. package/server/lib/session-title/DOCUMENTATION.md +40 -7
  218. package/server/lib/session-title/native-runtime.test.js +167 -0
  219. package/server/lib/session-title/runtime.js +67 -42
  220. package/server/lib/session-title/session-access.js +46 -0
  221. package/server/lib/session-title/session-access.test.js +91 -0
  222. package/server/lib/session-turn-pages/DOCUMENTATION.md +9 -8
  223. package/server/lib/session-turn-pages/routes.js +17 -102
  224. package/server/lib/session-turn-pages/routes.test.js +281 -0
  225. package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
  226. package/server/lib/session-turn-pages/session-message-projection.js +281 -0
  227. package/server/lib/small-model/DOCUMENTATION.md +101 -147
  228. package/server/lib/small-model/catalog.js +31 -51
  229. package/server/lib/small-model/catalog.test.js +65 -10
  230. package/server/lib/small-model/custom-api.js +68 -0
  231. package/server/lib/small-model/custom-api.test.js +64 -0
  232. package/server/lib/small-model/index.js +124 -240
  233. package/server/lib/small-model/resolve.js +35 -120
  234. package/server/lib/small-model/resolve.test.js +20 -110
  235. package/server/lib/small-model/routes.js +1 -3
  236. package/server/lib/small-model/summary-settings.test.js +157 -253
  237. package/server/opencode-proxy.test.js +2 -0
  238. package/dist/assets/AssistantView-BTCkm8yk.js +0 -3
  239. package/dist/assets/MarkstreamRendererImpl-CnkodGJW.js +0 -3
  240. package/dist/assets/MobileSurface-BXLeiZ4d.js +0 -2
  241. package/dist/assets/OverlayScrollbar-pMwxaCca.js +0 -2
  242. package/dist/assets/SettingsView-B5I0KDTZ.js +0 -25
  243. package/dist/assets/appThemeRegistry-COGfxTFV.js +0 -1
  244. package/dist/assets/collapsible-BXRd5_Hr.js +0 -1
  245. package/dist/assets/context-menu-jGtH6fR9.js +0 -1
  246. package/dist/assets/es-c52LTHHe.js +0 -15
  247. package/dist/assets/fr-PisFbUE8.js +0 -13
  248. package/dist/assets/gitApi-ButwzQ0N.js +0 -9
  249. package/dist/assets/insertionBoundaries-DGs7S5zv.js +0 -1
  250. package/dist/assets/ja-C8Ikun3Z.js +0 -15
  251. package/dist/assets/ko-ClFys5o2.js +0 -15
  252. package/dist/assets/main-Dn9rpsgl.js +0 -169
  253. package/dist/assets/markdown-worker-CptoICmq.js +0 -2
  254. package/dist/assets/miniChat-BpjxVqgg.js +0 -2
  255. package/dist/assets/mobile-nMJCaNq1.js +0 -2
  256. package/dist/assets/multirun-BvSG94mW.js +0 -4
  257. package/dist/assets/pl-CnaKWN5F.js +0 -15
  258. package/dist/assets/platform-CWTIp02q.js +0 -4
  259. package/dist/assets/projectMeta-DOe49dM_.js +0 -3
  260. package/dist/assets/pt-BR-s8OpfRI6.js +0 -15
  261. package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +0 -2
  262. package/dist/assets/renderMobileApp-y-S8y8v_.js +0 -28
  263. package/dist/assets/runtimeEndpointReset-B6BxhvR0.js +0 -147
  264. package/dist/assets/sessionChangeSummary-VVBlEXuh.js +0 -2
  265. package/dist/assets/src-BzX5UI-c.css +0 -2
  266. package/dist/assets/store-DJczmczv.js +0 -15
  267. package/dist/assets/textarea-3XR8sUfy.js +0 -1
  268. package/dist/assets/uk-CaX0iqkB.js +0 -15
  269. package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +0 -1
  270. package/dist/assets/useEffectiveDirectory-CN6JoCFw.js +0 -1
  271. package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +0 -414
  272. package/dist/assets/useSessionAutoCleanup-BLjymaFt.js +0 -2
  273. package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
  274. package/dist/assets/zh-CN-20MYEjha.js +0 -15
  275. package/dist/assets/zh-TW-DiN04qTw.js +0 -15
  276. package/server/lib/small-model/call.js +0 -427
  277. package/server/lib/small-model/call.test.js +0 -382
  278. package/server/lib/small-model/opencode-session.js +0 -183
  279. 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;
@@ -412,6 +412,11 @@ export const createNotificationTriggerRuntime = (deps) => {
412
412
  }
413
413
  }
414
414
  }
415
+ // Ticket 07: shutdown interrupt is not a completion terminal — no ready push.
416
+ if (payload.type === 'session.execution.interrupted') {
417
+ const reason = payload.properties?.reason ?? payload.data?.reason;
418
+ if (reason === 'shutdown') return;
419
+ }
415
420
  if ((payload.type === 'session.idle' || payload.type === 'session.error') && sessionId) {
416
421
  const error = payload.properties?.error;
417
422
  const errorText = typeof error?.message === 'string'