@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
@@ -50,7 +50,50 @@ const createDependencies = (getCommandSources, configDirectory, getAgentSources
50
50
  getOpenCodePort: () => 4096,
51
51
  });
52
52
 
53
+ describe('V2 configuration persistence', () => {
54
+ it.each(['agents', 'commands', 'mcp'])('%s CRUD never restarts or reloads OpenCode', async (entity) => {
55
+ const dependencies = createDependencies(vi.fn());
56
+ dependencies.refreshOpenCodeAfterConfigChange.mockRejectedValue(new Error('must not manage the service'));
57
+ const app = express();
58
+ app.use(express.json());
59
+ registerConfigEntityRoutes(app, dependencies);
60
+ for (const method of ['post', 'patch', 'delete']) {
61
+ const response = await request(app)[method](`/api/config/${entity}/example?directory=%2Frepo`)
62
+ .send({ name: 'example', scope: 'project', type: 'local', command: ['example'] });
63
+ expect(response.status).toBe(200);
64
+ expect(response.body).toMatchObject({ success: true, requiresReload: false, application: 'watch' });
65
+ expect(response.body.requiresManualRestart).not.toBe(true);
66
+ }
67
+ expect(dependencies.refreshOpenCodeAfterConfigChange).not.toHaveBeenCalled();
68
+ });
69
+ });
70
+
53
71
  describe('config entity command metadata route', () => {
72
+ it('preserves real SDK authentication and request options when fetching the catalog', async () => {
73
+ const { OpenCode: realOpenCode } = await vi.importActual('@opencode/client');
74
+ createOpencodeClient.mockImplementation(realOpenCode.make);
75
+ const upstream = vi.spyOn(globalThis, 'fetch').mockImplementation(async (_input, init) => {
76
+ if (new Headers(init?.headers).get('Authorization') !== 'Basic example') {
77
+ return new Response('Unauthorized', { status: 401 });
78
+ }
79
+ return Response.json({ data: [{ name: 'review', description: 'Review changes' }] });
80
+ });
81
+ try {
82
+ const app = express();
83
+ app.use(express.json());
84
+ registerConfigEntityRoutes(app, createDependencies(() => ({ md: { exists: false }, json: { exists: false } })));
85
+ const response = await request(app).post('/api/config/commands/metadata').send({ catalog: true });
86
+ expect(response.status).toBe(200);
87
+ expect(response.body.commands).toEqual([expect.objectContaining({ name: 'review' })]);
88
+ const [url, init] = upstream.mock.calls[0];
89
+ expect(new URL(url).pathname).toBe('/api/command');
90
+ expect(init.method).toBe('GET');
91
+ expect(init.signal).toBeInstanceOf(AbortSignal);
92
+ } finally {
93
+ upstream.mockRestore();
94
+ }
95
+ });
96
+
54
97
  it('returns metadata for many commands through one request', async () => {
55
98
  const app = express();
56
99
  app.use(express.json());
@@ -203,6 +246,70 @@ describe('provider catalog route', () => {
203
246
  expect(response.body).toEqual({ schemaVersion: 1, providers: [], default: {}, partial: false });
204
247
  });
205
248
 
249
+ it('projects ModelInfo.id as the catalog model id and default', async () => {
250
+ const app = express();
251
+ createOpencodeClient.mockReturnValue({
252
+ provider: { list: vi.fn(async () => ({ data: [{ id: 'openai', name: 'OpenAI' }] })) },
253
+ model: {
254
+ list: vi.fn(async () => ({
255
+ data: [{
256
+ id: 'gpt-5.4-mini',
257
+ modelID: 'internal-pack',
258
+ providerID: 'openai',
259
+ name: 'GPT-5.4 Mini',
260
+ }],
261
+ })),
262
+ default: vi.fn(async () => ({
263
+ data: { id: 'gpt-5.4-mini', modelID: 'internal-pack', providerID: 'openai', name: 'GPT-5.4 Mini' },
264
+ })),
265
+ },
266
+ });
267
+ registerConfigEntityRoutes(app, createDependencies(vi.fn()));
268
+
269
+ const response = await request(app).get('/api/config/catalog/providers').expect(200);
270
+ expect(response.body.providers).toEqual([{
271
+ id: 'openai',
272
+ name: 'OpenAI',
273
+ models: { 'gpt-5.4-mini': { id: 'gpt-5.4-mini', name: 'GPT-5.4 Mini' } },
274
+ }]);
275
+ expect(response.body.default).toEqual({ openai: 'gpt-5.4-mini' });
276
+ expect(JSON.stringify(response.body)).not.toContain('internal-pack');
277
+ });
278
+
279
+ it('projects v2 thinking variants into the provider catalog and strips request payloads', async () => {
280
+ const app = express();
281
+ createOpencodeClient.mockReturnValue({
282
+ provider: { list: vi.fn(async () => ({ data: [{ id: 'openai', name: 'OpenAI' }] })) },
283
+ model: {
284
+ list: vi.fn(async () => ({
285
+ data: [{
286
+ id: 'gpt-5.4',
287
+ modelID: 'internal-pack',
288
+ providerID: 'openai',
289
+ name: 'GPT 5.4',
290
+ capabilities: { tools: true, input: ['text', 'image'], output: ['text'] },
291
+ variants: [
292
+ { id: 'low', settings: { reasoningEffort: 'low', apiKey: 'variant-settings-sentinel' } },
293
+ { id: 'high', headers: { Authorization: 'variant-headers-sentinel' }, body: { store: false } },
294
+ ],
295
+ }],
296
+ })),
297
+ default: vi.fn(async () => ({ data: null })),
298
+ },
299
+ });
300
+ registerConfigEntityRoutes(app, createDependencies(vi.fn()));
301
+
302
+ const response = await request(app).get('/api/config/catalog/providers').expect(200);
303
+ expect(response.body.providers[0].models['gpt-5.4'].variants).toEqual({ low: {}, high: {} });
304
+ expect(response.body.providers[0].models['gpt-5.4'].capabilities).toEqual({
305
+ toolcall: true,
306
+ input: { text: true, image: true },
307
+ output: { text: true },
308
+ });
309
+ expect(JSON.stringify(response.body)).not.toContain('sentinel');
310
+ expect(JSON.stringify(response.body)).not.toContain('internal-pack');
311
+ });
312
+
206
313
  it('returns 502 for SDK failure and malformed catalog responses', async () => {
207
314
  const app = express();
208
315
  createOpencodeClient.mockReturnValue({
@@ -0,0 +1,105 @@
1
+ import { test, expect } from 'vitest';
2
+ import { OpenCode } from '@opencode/client';
3
+ import { spawn } from 'node:child_process';
4
+ import { once } from 'node:events';
5
+ import { randomBytes } from 'node:crypto';
6
+ import fs from 'node:fs/promises';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+
10
+ // Opt-in, isolated real V2 server. No user config, credentials, model calls,
11
+ // shared service registration, or npm plugin installs are used.
12
+ test.skipIf(!process.env.OPENCHAMBER_TEST_OPENCODE_BINARY)('V2 applies credentials, global config and plugin changes in the same process', async () => {
13
+ const temporary = path.join(os.tmpdir(), 'opencode');
14
+ await fs.mkdir(temporary, { recursive: true });
15
+ const root = await fs.mkdtemp(path.join(temporary, 'config-hot-reload-'));
16
+ const config = path.join(root, 'config', 'opencode');
17
+ const directory = path.join(root, 'project');
18
+ await fs.mkdir(config, { recursive: true });
19
+ await fs.mkdir(directory, { recursive: true });
20
+ const password = randomBytes(24).toString('hex');
21
+ const child = spawn(process.env.OPENCHAMBER_TEST_OPENCODE_BINARY, ['serve', '--hostname', '127.0.0.1', '--port', '0'], {
22
+ cwd: directory,
23
+ env: {
24
+ PATH: process.env.PATH,
25
+ HOME: root,
26
+ TMPDIR: temporary,
27
+ XDG_CONFIG_HOME: path.join(root, 'config'),
28
+ XDG_DATA_HOME: path.join(root, 'data'),
29
+ XDG_STATE_HOME: path.join(root, 'state'),
30
+ XDG_CACHE_HOME: path.join(root, 'cache'),
31
+ OPENCODE_CONFIG_DIR: config,
32
+ OPENCODE_SERVER_PASSWORD: password,
33
+ OPENCODE_DISABLE_MODELS_FETCH: '1',
34
+ },
35
+ stdio: ['ignore', 'pipe', 'pipe'],
36
+ });
37
+ let output = '';
38
+ let spawnError;
39
+ child.on('error', (error) => { spawnError = error; });
40
+ child.stdout.on('data', (data) => { output = (output + data).slice(-8192); });
41
+ child.stderr.on('data', () => undefined);
42
+ const closed = once(child, 'close').catch(() => undefined);
43
+ const until = async (read, accepts, label) => {
44
+ const deadline = Date.now() + 20_000;
45
+ while (Date.now() < deadline) {
46
+ if (spawnError) throw spawnError;
47
+ if (child.exitCode !== null) throw new Error(`Isolated OpenCode exited during ${label}`);
48
+ const value = await read();
49
+ if (accepts(value)) return value;
50
+ await new Promise((resolve) => setTimeout(resolve, 100));
51
+ }
52
+ throw new Error(`Timed out waiting for ${label}`);
53
+ };
54
+ const write = async (info) => {
55
+ const file = path.join(config, 'opencode.json');
56
+ await fs.writeFile(`${file}.tmp`, JSON.stringify(info));
57
+ await fs.rename(`${file}.tmp`, file);
58
+ };
59
+ try {
60
+ const origin = await until(
61
+ async () => output.match(/(?:server listening on|opencode server listening on) (http:\/\/[^\s]+)/)?.[1],
62
+ Boolean, 'listener',
63
+ );
64
+ const client = OpenCode.make({
65
+ baseUrl: origin,
66
+ headers: { Authorization: `Basic ${Buffer.from(`opencode:${password}`).toString('base64')}` },
67
+ fetch: (input, init) => fetch(input, { ...init, signal: AbortSignal.timeout(15_000) }),
68
+ });
69
+ const location = { directory };
70
+ await client.command.list({ location });
71
+ const pid = child.pid;
72
+ const commands = { 'hot-reload-probe': { template: 'Do not execute anything.', description: 'one' } };
73
+ await write({ commands });
74
+ await until(() => client.command.list({ location }), (result) => result.data.some((entry) => entry.name === 'hot-reload-probe'), 'global command create');
75
+ await write({ commands: { 'hot-reload-probe': { ...commands['hot-reload-probe'], description: 'two' } } });
76
+ await until(() => client.command.list({ location }), (result) => result.data.some((entry) => entry.name === 'hot-reload-probe' && entry.description === 'two'), 'command update');
77
+
78
+ await write({ commands, plugins: ['*', '-opencode.config.command'] });
79
+ await until(() => client.command.list({ location }), (result) => !result.data.some((entry) => entry.name === 'hot-reload-probe'), 'plugin disable');
80
+ await write({ commands });
81
+ await until(() => client.command.list({ location }), (result) => result.data.some((entry) => entry.name === 'hot-reload-probe'), 'plugin re-enable');
82
+
83
+ await client.integration.connect.key({ integrationID: 'anthropic', key: 'isolated-test-placeholder', location });
84
+ const integration = await until(() => client.integration.get({ integrationID: 'anthropic', location }),
85
+ (result) => result.data.connections.some((connection) => connection.type === 'credential'), 'credential connection');
86
+ for (const connection of integration.data.connections) {
87
+ if (connection.type === 'credential') await client.credential.remove({ credentialID: connection.id });
88
+ }
89
+ await until(() => client.integration.get({ integrationID: 'anthropic', location }),
90
+ (result) => result.data.connections.length === 0, 'credential removal');
91
+ await write({ commands, mcp: { servers: { 'hot-reload-mcp': { type: 'local', command: ['unused-test-command'], disabled: true } } } });
92
+ await until(() => client.mcp.list({ location }), (result) => result.data.some((server) => server.name === 'hot-reload-mcp'), 'MCP config add');
93
+ await write({});
94
+ await until(() => client.mcp.list({ location }), (result) => !result.data.some((server) => server.name === 'hot-reload-mcp'), 'MCP config removal');
95
+ await until(() => client.command.list({ location }), (result) => !result.data.some((entry) => entry.name === 'hot-reload-probe'), 'command removal');
96
+ expect(child.pid).toBe(pid);
97
+ expect(child.exitCode).toBeNull();
98
+ } finally {
99
+ child.kill('SIGTERM');
100
+ const kill = setTimeout(() => child.kill('SIGKILL'), 5000);
101
+ await closed;
102
+ clearTimeout(kill);
103
+ await fs.rm(root, { recursive: true, force: true });
104
+ }
105
+ }, 120_000);
@@ -180,8 +180,7 @@ export async function installPinnedOpenCode2Cli(options = {}) {
180
180
  });
181
181
  }
182
182
 
183
- const extractedBinary = findBinary(extractDir, binaryName)
184
- || findBinary(extractDir, platform === 'win32' ? 'opencode2.exe' : 'opencode2');
183
+ const extractedBinary = findBinary(extractDir, binaryName);
185
184
  if (!extractedBinary) {
186
185
  throw new Error(`Archive ${archivePath} did not contain ${binaryName} or opencode`);
187
186
  }
@@ -314,14 +314,6 @@ export const createOpenCodeEnvRuntime = (deps) => {
314
314
  return trimmed;
315
315
  };
316
316
 
317
- const createOpenCode1xVersionError = (candidate, version) => {
318
- const error = new Error(
319
- `OpenCode 1.x is not supported (${candidate} reports ${version}). Install OpenCode v2 (opencode).`
320
- );
321
- error.code = 'OPENCODE_BINARY_INVALID';
322
- return error;
323
- };
324
-
325
317
  const acceptOpenCodeV2Candidate = (candidate, source) => {
326
318
  if (typeof candidate !== 'string' || !candidate.trim()) return null;
327
319
  if (!isExecutable(candidate) || isWindowsOpenCodeDesktopAppPath(candidate) || isMacOpenCodeAppBundlePath(candidate)) {
@@ -353,22 +345,25 @@ export const createOpenCodeEnvRuntime = (deps) => {
353
345
  if (!isExecutable(candidate)) continue;
354
346
  const version = readOpenCode2BinaryVersion(candidate);
355
347
  if (isOpenCode1xVersion(version)) {
356
- throw createOpenCode1xVersionError(candidate, version);
348
+ // Inherited OPENCODE_BINARY often points at a still-installed 1.x.
349
+ // Skip it and keep looking; never overwrite that binary.
350
+ console.warn(`Skipping OpenCode 1.x at ${candidate} (${version}); looking for v2.`);
351
+ continue;
357
352
  }
358
353
  const accepted = acceptOpenCodeV2Candidate(candidate, 'env');
359
354
  if (accepted) return accepted;
360
355
  }
361
356
 
362
357
  // Match master: reuse a globally installed CLI. Official v2 is `opencode`;
363
- // `opencode2` remains a compatibility alias. Skip 1.x binaries by version.
364
- for (const name of process.platform === 'win32' ? ['opencode', 'opencode2'] : ['opencode', 'opencode2']) {
358
+ // Skip 1.x binaries by version; auto-discovery uses only the official name.
359
+ for (const name of ['opencode']) {
365
360
  const resolvedFromPath = searchPathFor(name);
366
361
  const accepted = acceptOpenCodeV2Candidate(resolvedFromPath, 'path');
367
362
  if (accepted) return accepted;
368
363
  }
369
364
 
370
365
  const home = resolveHomeDir();
371
- const unixFallbacks = ['opencode', 'opencode2'].flatMap((name) => [
366
+ const unixFallbacks = ['opencode'].flatMap((name) => [
372
367
  path.join(home, '.opencode', 'bin', name),
373
368
  path.join(home, '.bun', 'bin', name),
374
369
  path.join(home, '.local', 'bin', name),
@@ -386,7 +381,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
386
381
  const localAppData = process.env.LOCALAPPDATA || '';
387
382
  const programData = process.env.ProgramData || 'C:\\ProgramData';
388
383
  const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
389
- const names = ['opencode.exe', 'opencode.cmd', 'opencode2.exe', 'opencode2.cmd'];
384
+ const names = ['opencode.exe', 'opencode.cmd'];
390
385
  const dirs = [
391
386
  path.join(userProfile, '.opencode', 'bin'),
392
387
  path.join(appData, 'npm'),
@@ -405,7 +400,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
405
400
  }
406
401
 
407
402
  if (process.platform === 'win32') {
408
- for (const name of ['opencode', 'opencode2']) {
403
+ for (const name of ['opencode']) {
409
404
  try {
410
405
  const result = runSpawnSync('where', [name], {
411
406
  encoding: 'utf8',
@@ -432,7 +427,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
432
427
  }
433
428
 
434
429
  const shells = [process.env.SHELL, '/bin/zsh', '/bin/bash', '/bin/sh'].filter(Boolean);
435
- for (const name of ['opencode', 'opencode2']) {
430
+ for (const name of ['opencode']) {
436
431
  for (const shell of shells) {
437
432
  if (!isExecutable(shell)) continue;
438
433
  try {
@@ -1023,10 +1018,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
1023
1018
  if (normalized && isExecutable(normalized)) {
1024
1019
  const version = readOpenCode2BinaryVersion(normalized);
1025
1020
  if (isOpenCode1xVersion(version)) {
1026
- if (strict) {
1027
- throw createOpenCode1xVersionError(normalized, version);
1028
- }
1029
- console.warn(`Configured settings.opencodeBinary points at OpenCode 1.x, which OpenChamber refuses: ${normalized}`);
1021
+ console.warn(`Skipping configured OpenCode 1.x at ${normalized} (${version}); looking for v2.`);
1030
1022
  return null;
1031
1023
  }
1032
1024
  }
@@ -1061,15 +1053,16 @@ export const createOpenCodeEnvRuntime = (deps) => {
1061
1053
  if (state.useWslForOpencode) {
1062
1054
  return state.resolvedOpencodeBinary;
1063
1055
  }
1056
+ process.env.OPENCODE_BINARY = state.resolvedOpencodeBinary;
1064
1057
  ensureOpencodeShimRuntime(state.resolvedOpencodeBinary);
1065
1058
  return state.resolvedOpencodeBinary;
1066
1059
  }
1067
1060
 
1068
1061
  const existing = typeof process.env.OPENCODE_BINARY === 'string' ? process.env.OPENCODE_BINARY.trim() : '';
1069
1062
  if (existing && isExecutable(existing) && isOpenCode1xVersion(readOpenCode2BinaryVersion(existing))) {
1070
- throw createOpenCode1xVersionError(existing, readOpenCode2BinaryVersion(existing));
1071
- }
1072
- if (existing && isExecutable(existing)) {
1063
+ console.warn(`Ignoring OpenCode 1.x OPENCODE_BINARY at ${existing}; looking for v2.`);
1064
+ delete process.env.OPENCODE_BINARY;
1065
+ } else if (existing && isExecutable(existing)) {
1073
1066
  clearWslOpencodeResolution();
1074
1067
  state.resolvedOpencodeBinary = existing;
1075
1068
  state.resolvedOpencodeBinarySource = state.resolvedOpencodeBinarySource || 'env';
@@ -1113,8 +1106,8 @@ export const createOpenCodeEnvRuntime = (deps) => {
1113
1106
  dirs.push(path.join(process.resourcesPath, 'opencode-cli'));
1114
1107
  }
1115
1108
 
1116
- // Official v2 name + packaging contract name (see electron opencode2-bundle-contract).
1117
- const names = ['opencode', 'opencode.exe', 'opencode2', 'opencode2.exe'];
1109
+ // Runtime and packaging use the official v2 executable name.
1110
+ const names = ['opencode', 'opencode.exe'];
1118
1111
  for (const dir of dirs) {
1119
1112
  for (const name of names) {
1120
1113
  const accepted = acceptOpenCodeV2Candidate(path.join(dir, name), 'bundled');
@@ -1141,11 +1134,11 @@ export const createOpenCodeEnvRuntime = (deps) => {
1141
1134
  if (!result?.path) {
1142
1135
  return ensureOpencodeCliEnv();
1143
1136
  }
1144
- if (result.source === 'installed') {
1137
+ if (result.path) {
1145
1138
  clearWslOpencodeResolution();
1146
1139
  process.env.OPENCODE_BINARY = result.path;
1147
1140
  state.resolvedOpencodeBinary = result.path;
1148
- state.resolvedOpencodeBinarySource = 'installed';
1141
+ state.resolvedOpencodeBinarySource = result.source === 'installed' ? 'installed' : (state.resolvedOpencodeBinarySource || 'discovered');
1149
1142
  }
1150
1143
  return ensureOpencodeCliEnv();
1151
1144
  } catch (error) {
@@ -1241,6 +1234,26 @@ export const createOpenCodeEnvRuntime = (deps) => {
1241
1234
  state.resolvedOpencodeBinary = null;
1242
1235
  };
1243
1236
 
1237
+ /**
1238
+ * Pin managed launch to an explicit binary for the duration of an owned-cache
1239
+ * upgrade (ticket 12). Avoids rediscovery that would re-attach a global CLI.
1240
+ */
1241
+ const forceResolvedOpenCodeBinary = (binaryPath, source = 'installed') => {
1242
+ const trimmed = typeof binaryPath === 'string' ? binaryPath.trim() : '';
1243
+ if (!trimmed) {
1244
+ const error = new Error('forceResolvedOpenCodeBinary requires a binary path');
1245
+ error.code = 'OPENCODE_BINARY_INVALID';
1246
+ throw error;
1247
+ }
1248
+ clearWslOpencodeResolution();
1249
+ process.env.OPENCODE_BINARY = trimmed;
1250
+ state.resolvedOpencodeBinary = trimmed;
1251
+ state.resolvedOpencodeBinarySource = source || 'installed';
1252
+ prependToPath(path.dirname(trimmed));
1253
+ ensureOpencodeShimRuntime(trimmed);
1254
+ return trimmed;
1255
+ };
1256
+
1244
1257
  return {
1245
1258
  applyLoginShellEnvSnapshot,
1246
1259
  ensureOpencodeCliEnv,
@@ -1253,5 +1266,6 @@ export const createOpenCodeEnvRuntime = (deps) => {
1253
1266
  searchPathFor,
1254
1267
  resolveGitBinaryForSpawn,
1255
1268
  clearResolvedOpenCodeBinary,
1269
+ forceResolvedOpenCodeBinary,
1256
1270
  };
1257
1271
  };
@@ -168,31 +168,24 @@ describe('OpenCode env runtime', () => {
168
168
  expect(state.resolvedOpencodeBinarySource).toBe('settings');
169
169
  });
170
170
 
171
- it('rejects a configured 1.x opencode version in strict mode', async () => {
171
+ it('skips a configured 1.x opencode version instead of aborting startup', async () => {
172
172
  const dir = createTempDir('openchamber-opencode-1x-');
173
173
  const binary = path.join(dir, 'opencode');
174
174
  writeVersionBinary(binary, '1.18.4');
175
175
  const { runtime } = createRuntime({ opencodeBinary: binary });
176
176
 
177
- await expect(runtime.applyOpencodeBinaryFromSettings({ strict: true })).rejects.toMatchObject({
178
- code: 'OPENCODE_BINARY_INVALID',
179
- message: expect.stringMatching(/1\.x.*OpenCode v2/s),
180
- });
177
+ await expect(runtime.applyOpencodeBinaryFromSettings({ strict: true })).resolves.toBeNull();
178
+ expect(process.env.OPENCODE_BINARY).not.toBe(binary);
181
179
  });
182
180
 
183
- it('rejects an explicit OPENCODE_BINARY that reports a 1.x version', () => {
181
+ it('skips an explicit OPENCODE_BINARY that reports a 1.x version', () => {
184
182
  const dir = createTempDir('openchamber-env-opencode-1x-');
185
183
  const binary = path.join(dir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
186
184
  writeVersionBinary(binary, '1.18.4');
187
185
  process.env.OPENCODE_BINARY = binary;
188
186
  const { runtime } = createRuntime({});
189
187
 
190
- expect(() => runtime.resolveOpencodeCliPath()).toThrow(
191
- expect.objectContaining({
192
- code: 'OPENCODE_BINARY_INVALID',
193
- message: expect.stringMatching(/1\.x.*OpenCode v2/s),
194
- })
195
- );
188
+ expect(runtime.resolveOpencodeCliPath()).not.toBe(binary);
196
189
  });
197
190
 
198
191
  it('discovers opencode from a home-directory install location', () => {
@@ -233,7 +226,7 @@ describe('OpenCode env runtime', () => {
233
226
 
234
227
  it('prefers a user-installed OpenCode from PATH over the bundled CLI', () => {
235
228
  const bundledDir = createTempDir('openchamber-bundled-opencode-');
236
- const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? 'opencode2.exe' : 'opencode2');
229
+ const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
237
230
  const pathDir = createTempDir('openchamber-path-opencode-');
238
231
  const pathBinary = path.join(pathDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
239
232
  fs.writeFileSync(bundledBinary, '#!/bin/sh\nexit 0\n');
@@ -252,7 +245,7 @@ describe('OpenCode env runtime', () => {
252
245
 
253
246
  it('keeps explicit OpenCode binary ahead of bundled CLI', () => {
254
247
  const bundledDir = createTempDir('openchamber-bundled-opencode-');
255
- const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? 'opencode2.exe' : 'opencode2');
248
+ const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
256
249
  const explicitDir = createTempDir('openchamber-explicit-opencode-');
257
250
  const explicitBinary = path.join(explicitDir, process.platform === 'win32' ? 'opencode2.exe' : 'opencode2');
258
251
  fs.writeFileSync(bundledBinary, '#!/bin/sh\nexit 0\n');
@@ -271,7 +264,7 @@ describe('OpenCode env runtime', () => {
271
264
  it('does not use a bundled OpenCode CLI from Electron resourcesPath', () => {
272
265
  const resourcesPath = createTempDir('openchamber-resources-');
273
266
  const bundledDir = path.join(resourcesPath, 'opencode-cli');
274
- const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? 'opencode2.exe' : 'opencode2');
267
+ const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
275
268
  fs.mkdirSync(bundledDir, { recursive: true });
276
269
  fs.writeFileSync(bundledBinary, '#!/bin/sh\nexit 0\n');
277
270
  if (process.platform !== 'win32') {
@@ -346,7 +339,7 @@ describe('OpenCode env runtime', () => {
346
339
 
347
340
  it('falls back to a bundled 2.x CLI only after pin install fails', async () => {
348
341
  const bundledDir = createTempDir('openchamber-bundled-fallback-');
349
- const bundledName = process.platform === 'win32' ? 'opencode2.exe' : 'opencode2';
342
+ const bundledName = process.platform === 'win32' ? 'opencode.exe' : 'opencode';
350
343
  const bundledBinary = path.join(bundledDir, bundledName);
351
344
  writeVersionBinary(bundledBinary, '2.0.12');
352
345
  process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR = bundledDir;
@@ -395,7 +388,7 @@ describe('OpenCode env runtime', () => {
395
388
 
396
389
  it('does not adopt a bundled 1.x CLI after pin install fails', async () => {
397
390
  const bundledDir = createTempDir('openchamber-bundled-1x-');
398
- const bundledName = process.platform === 'win32' ? 'opencode2.exe' : 'opencode2';
391
+ const bundledName = process.platform === 'win32' ? 'opencode.exe' : 'opencode';
399
392
  const bundledBinary = path.join(bundledDir, bundledName);
400
393
  writeVersionBinary(bundledBinary, '1.18.4');
401
394
  process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR = bundledDir;
@@ -25,7 +25,7 @@ import { getNpmInfo, clearCache as clearNpmCache } from './npm-registry.js';
25
25
  import { parseNpmSpec, parsePathSpec, isExactSemver } from './plugin-spec.js';
26
26
  import { registerOpenCodeRoutes } from './routes.js';
27
27
  import { getProviderSources, removeProviderConfig } from './providers.js';
28
- import { getAgentSources, getAgentConfig, createAgent, updateAgent, deleteAgent } from './agents.js';
28
+ import { getAgentSources, getAgentConfig, listDisabledAgentOverrides, createAgent, updateAgent, deleteAgent } from './agents.js';
29
29
  import { getCommandSources, createCommand, updateCommand, deleteCommand } from './commands.js';
30
30
  import { listMcpConfigs, getMcpConfig, createMcpConfig, updateMcpConfig, deleteMcpConfig } from './mcp.js';
31
31
  import { listSnippets, getSnippet, createSnippet, updateSnippet, deleteSnippet, expandSnippets } from './snippets.js';
@@ -115,6 +115,15 @@ export const createFeatureRoutesRuntime = (dependencies) => {
115
115
  buildOpenCodeUrl,
116
116
  getOpenCodeAuthHeaders,
117
117
  getIsExternalOpenCode = () => false,
118
+ getIsSharedOpenCodeService = () => false,
119
+ forceResolvedOpenCodeBinary = null,
120
+ restartOpenCode = null,
121
+ getRuntimeContract = () => null,
122
+ getOpenCodeServeVersion = () => null,
123
+ getOpenCodeCliVersion = () => null,
124
+ getResolvedOpenCodeBinary = () => null,
125
+ getResolvedOpenCodeBinarySource = () => null,
126
+ getActiveSessionCount = () => 0,
118
127
  getSmallModelService: routeGetSmallModelService,
119
128
  getOpenCodePort,
120
129
  buildAugmentedPath,
@@ -135,8 +144,11 @@ export const createFeatureRoutesRuntime = (dependencies) => {
135
144
  sessionIndexService,
136
145
  notifyContactTurnComplete,
137
146
  sessionMetadataStore = null,
147
+ sessionArchiveService = null,
138
148
  onSessionMetadataWritten = null,
139
149
  persistSessionGoal = null,
150
+ persistSessionMetadata = null,
151
+ onSystemSessionPersisted = null,
140
152
  readSessionMetadata = null,
141
153
  } = routeDependencies;
142
154
 
@@ -171,6 +183,17 @@ export const createFeatureRoutesRuntime = (dependencies) => {
171
183
  buildOpenCodeUrl,
172
184
  getOpenCodeAuthHeaders,
173
185
  getIsExternalOpenCode,
186
+ getIsSharedOpenCodeService,
187
+ forceResolvedOpenCodeBinary,
188
+ restartOpenCode,
189
+ waitForOpenCodeReady,
190
+ getRuntimeContract,
191
+ getOpenCodeServeVersion,
192
+ getOpenCodeCliVersion,
193
+ getResolvedOpenCodeBinary,
194
+ getResolvedOpenCodeBinarySource,
195
+ getActiveSessionCount,
196
+ openchamberDataDir,
174
197
  onSettingsPersisted: (updated, changes) => {
175
198
  if (!questionAutoDelegateRuntime) return;
176
199
  if (!Object.prototype.hasOwnProperty.call(changes ?? {}, 'questionAutoDelegateEnabled')) return;
@@ -254,6 +277,8 @@ export const createFeatureRoutesRuntime = (dependencies) => {
254
277
  registerLlmRoutes(app, {
255
278
  buildOpenCodeUrl,
256
279
  getOpenCodeAuthHeaders,
280
+ persistSessionMetadata,
281
+ onSystemSessionPersisted,
257
282
  });
258
283
  assistantRoutesRuntime = registerAssistantRoutes(app, {
259
284
  openchamberDataDir,
@@ -280,6 +305,18 @@ export const createFeatureRoutesRuntime = (dependencies) => {
280
305
  },
281
306
  listScheduledTasks: (projectID) => projectConfigRuntime.listScheduledTasks(projectID),
282
307
  sessionIndexService,
308
+ archiveSessionHost: sessionArchiveService
309
+ ? async ({ sessionID, directory, archivedAt }) => sessionArchiveService.archiveSession(
310
+ sessionID,
311
+ directory,
312
+ archivedAt,
313
+ )
314
+ : null,
315
+ forgetSessionHost: sessionArchiveService
316
+ ? (sessionID) => sessionArchiveService.forgetSession(sessionID)
317
+ : null,
318
+ persistSessionMetadata,
319
+ onSystemSessionPersisted,
283
320
  upsertScheduledTask: (projectID, task) => projectConfigRuntime.upsertScheduledTask(projectID, task),
284
321
  syncScheduledTaskProject: (projectID) => scheduledTasksRuntime.syncProject(projectID),
285
322
  refreshAllowedRoots: refreshAssistantAllowedRoots,
@@ -301,9 +338,10 @@ export const createFeatureRoutesRuntime = (dependencies) => {
301
338
  refreshOpenCodeAfterConfigChange,
302
339
  clientReloadDelayMs,
303
340
  waitForOpenCodeReady,
304
- getAgentSources,
305
- getAgentConfig,
306
- createAgent,
341
+ getAgentSources,
342
+ getAgentConfig,
343
+ listDisabledAgentOverrides,
344
+ createAgent,
307
345
  updateAgent,
308
346
  deleteAgent,
309
347
  getCommandSources,
@@ -420,6 +458,7 @@ export const createFeatureRoutesRuntime = (dependencies) => {
420
458
  if (sessionMetadataStore) {
421
459
  registerSessionMetadataRoutes(app, {
422
460
  sessionMetadataStore,
461
+ sessionArchiveService,
423
462
  broadcastGlobalUiEvent,
424
463
  onMetadataWritten: onSessionMetadataWritten,
425
464
  });
@@ -3,6 +3,20 @@ import { describe, expect, it, vi } from 'vitest';
3
3
  import { createWorktreeTopologyBroadcaster } from './feature-routes-runtime.js';
4
4
 
5
5
  describe('feature routes runtime composition', () => {
6
+ it('passes live shared-service ownership from the host to upgrade routes', async () => {
7
+ const host = await fs.readFile(new URL('../../index.js', import.meta.url), 'utf8');
8
+ const routes = await fs.readFile(new URL('./feature-routes-runtime.js', import.meta.url), 'utf8');
9
+ expect(host).toContain('getIsSharedOpenCodeService: () => openCodeLifecycleRuntime.isSharedOpenCodeService()');
10
+ expect(routes).toMatch(/getIsSharedOpenCodeService = \(\) => false,[\s\S]*\} = routeDependencies;/);
11
+ expect(routes).toMatch(/registerOpenCodeRoutes\(app, \{[^}]*getIsSharedOpenCodeService,/);
12
+ });
13
+
14
+ it('wires Host isolation persist into scheduled-task runtime from the composition root', async () => {
15
+ const source = await fs.readFile(new URL('../../index.js', import.meta.url), 'utf8');
16
+ expect(source).toMatch(/createScheduledTasksRuntime\(\{[\s\S]*persistSessionMetadata: persistSessionMetadataToStore/);
17
+ expect(source).toMatch(/createScheduledTasksRuntime\(\{[\s\S]*onSystemSessionPersisted: hideSystemSessionFromIndex/);
18
+ });
19
+
6
20
  it('wires scheduled-task upsert into assistant contact tools', async () => {
7
21
  const source = await fs.readFile(new URL('./feature-routes-runtime.js', import.meta.url), 'utf8');
8
22
  expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*upsertScheduledTask:[\s\S]*projectConfigRuntime\.upsertScheduledTask/);
@@ -10,6 +24,10 @@ describe('feature routes runtime composition', () => {
10
24
  expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*listProjects:/);
11
25
  expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*listScheduledTasks:[\s\S]*projectConfigRuntime\.listScheduledTasks/);
12
26
  expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*sessionIndexService/);
27
+ expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*persistSessionMetadata/);
28
+ expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*onSystemSessionPersisted/);
29
+ expect(source).toMatch(/registerLlmRoutes\(app, \{[\s\S]*persistSessionMetadata/);
30
+ expect(source).toMatch(/registerLlmRoutes\(app, \{[\s\S]*onSystemSessionPersisted/);
13
31
  expect(source).toMatch(/label: project\.label\.trim\(\)/);
14
32
  });
15
33