@openchambery/web 1.19.22-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 (311) hide show
  1. package/README.md +13 -7
  2. package/bin/cli-opencode.test.js +208 -0
  3. package/bin/cli.js +62 -9
  4. package/bin/lib/cli-startup.js +105 -2
  5. package/bin/lib/cli-startup.test.js +179 -0
  6. package/dist/assets/{AgentAvatar-CRaBJXKs.js → AgentAvatar-B_JQoreg.js} +1 -1
  7. package/dist/assets/AssistantView-ClQAfGOT.js +3 -0
  8. package/dist/assets/{DiagramView-D1LGK_s2.js → DiagramView-BNnda8qO.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-B0kJo9Ii.js → FadeInOnReveal-VYQTSAkr.js} +1 -1
  10. package/dist/assets/{Icon-BgdfPhNs.js → Icon-BiEQEcih.js} +1 -1
  11. package/dist/assets/MarkdownRenderer-CJHSsWin.js +2 -0
  12. package/dist/assets/{MarkdownRendererImpl-DfoCJ1Mq.js → MarkdownRendererImpl-DwrHFGUp.js} +3 -3
  13. package/dist/assets/{MarkstreamRendererImpl-D2dsW5Lg.js → MarkstreamRendererImpl-CSnbQMu3.js} +1 -1
  14. package/dist/assets/MobileDetailNavigation-BXr6AGLT.js +1 -0
  15. package/dist/assets/{MobileShareBridge-2Yjn6wXg.js → MobileShareBridge-D7OE9OP_.js} +2 -2
  16. package/dist/assets/MobileSurface-D2jUkX9l.js +2 -0
  17. package/dist/assets/{MultiRunWindow-tTbMFowS.js → MultiRunWindow-DF3iIFCd.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-CD2KtlF3.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-zIa76s4t.js → SettingsWindow-BI5swspb.js} +1 -1
  22. package/dist/assets/{TerminalView-CDdzF4yi.js → TerminalView-BU3x8-fr.js} +1 -1
  23. package/dist/assets/{ToolOutputDialog-DNqA4uBl.js → ToolOutputDialog-BEWhBcRd.js} +2 -2
  24. package/dist/assets/appThemeRegistry-4MR0F7Jz.js +1 -0
  25. package/dist/assets/{button-CI7Q-Zw_.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-DdLO8HZ9.js → diagram-D7T8HQgj.js} +1 -1
  31. package/dist/assets/{dialog-B6pnIYmp.js → dialog-DQYl3W_o.js} +1 -1
  32. package/dist/assets/es-CN3E_zXj.js +15 -0
  33. package/dist/assets/{fileMentionTouchSelection-DNeiqns-.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-D-__fwRi.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-BeveiW6F.js +2 -0
  44. package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
  45. package/dist/assets/{markdownHeightCache-BhxqJOvf.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-CMkwoQ_X.js → runtimeConfig-BIGBRGQI.js} +1 -1
  58. package/dist/assets/{runtimeEndpointReset-ChOxt-J7.js → runtimeEndpointReset-D2savkuw.js} +12 -12
  59. package/dist/assets/{sessionChangeSummary-NnxcqCTI.js → sessionChangeSummary-eIbpSfrO.js} +1 -1
  60. package/dist/assets/{sessionLookup-BrU3rFHJ.js → sessionLookup-myHe5ltu.js} +1 -1
  61. package/dist/assets/{sortable-tabs-strip-Fnfhf3K9.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-xjqssKke.js → terminalApi-BxcdiduX.js} +1 -1
  65. package/dist/assets/{textarea-bHYxZtfH.js → textarea-CakKvqFw.js} +1 -1
  66. package/dist/assets/{tooltip-0T5jv0cu.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-C02YFyAv.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-B1JYyzpK.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-C9z0ML1z.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-opencode-client-BOe2qpQb.js +12 -0
  89. package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
  90. package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
  91. package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
  92. package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
  93. package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
  94. package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
  95. package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
  96. package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
  97. package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
  98. package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
  99. package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
  100. package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
  101. package/dist/index.html +13 -13
  102. package/dist/mini-chat.html +13 -13
  103. package/dist/mobile.html +13 -13
  104. package/package.json +2 -2
  105. package/server/index.js +392 -20
  106. package/server/lib/assistants/DOCUMENTATION.md +26 -21
  107. package/server/lib/assistants/assign.js +2 -1
  108. package/server/lib/assistants/contact-store.js +37 -19
  109. package/server/lib/assistants/contracts.js +11 -1
  110. package/server/lib/assistants/projection-history.test.js +454 -0
  111. package/server/lib/assistants/routes.js +11 -5
  112. package/server/lib/assistants/service.js +850 -294
  113. package/server/lib/assistants/service.test.js +588 -274
  114. package/server/lib/conversations/DOCUMENTATION.md +5 -5
  115. package/server/lib/conversations/service.js +87 -57
  116. package/server/lib/conversations/service.test.js +132 -100
  117. package/server/lib/conversations/validation.js +11 -1
  118. package/server/lib/event-stream/directory-ws-bridge.js +13 -2
  119. package/server/lib/event-stream/global-hub.js +51 -5
  120. package/server/lib/event-stream/runtime.js +57 -20
  121. package/server/lib/event-stream/runtime.test.js +112 -0
  122. package/server/lib/llm/DOCUMENTATION.md +95 -84
  123. package/server/lib/llm/catalog.js +111 -45
  124. package/server/lib/llm/catalog.test.js +121 -59
  125. package/server/lib/llm/completions.js +41 -12
  126. package/server/lib/llm/completions.test.js +45 -4
  127. package/server/lib/llm/generate.js +516 -453
  128. package/server/lib/llm/generate.test.js +705 -484
  129. package/server/lib/llm/routes.js +8 -4
  130. package/server/lib/message-queue/DOCUMENTATION.md +5 -5
  131. package/server/lib/message-queue/opencode-adapter.js +191 -104
  132. package/server/lib/message-queue/opencode-adapter.test.js +328 -58
  133. package/server/lib/message-queue/runtime.js +15 -2
  134. package/server/lib/message-queue/runtime.test.js +6 -6
  135. package/server/lib/message-queue/worker.js +7 -1
  136. package/server/lib/notifications/runtime.js +5 -0
  137. package/server/lib/opencode/DOCUMENTATION.md +103 -16
  138. package/server/lib/opencode/agent-document.d.ts +28 -0
  139. package/server/lib/opencode/agent-document.js +280 -0
  140. package/server/lib/opencode/agent-document.test.js +137 -0
  141. package/server/lib/opencode/agents.js +96 -0
  142. package/server/lib/opencode/auth-state-runtime.js +5 -0
  143. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  144. package/server/lib/opencode/config-entity-routes.js +111 -102
  145. package/server/lib/opencode/config-entity-routes.test.js +123 -10
  146. package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
  147. package/server/lib/opencode/ensure-cli.js +238 -0
  148. package/server/lib/opencode/ensure-cli.test.js +119 -0
  149. package/server/lib/opencode/env-runtime.js +208 -111
  150. package/server/lib/opencode/env-runtime.test.js +211 -14
  151. package/server/lib/opencode/feature-routes-runtime.js +80 -5
  152. package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
  153. package/server/lib/opencode/http-server-connections.js +98 -0
  154. package/server/lib/opencode/http-server-connections.test.js +321 -0
  155. package/server/lib/opencode/lifecycle.js +539 -95
  156. package/server/lib/opencode/lifecycle.test.js +698 -6
  157. package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
  158. package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
  159. package/server/lib/opencode/network-runtime.js +28 -16
  160. package/server/lib/opencode/network-runtime.test.js +68 -4
  161. package/server/lib/opencode/openchamber-routes.js +7 -1
  162. package/server/lib/opencode/opencode2-pin.js +139 -0
  163. package/server/lib/opencode/opencode2-pin.test.js +88 -0
  164. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +326 -0
  165. package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
  166. package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
  167. package/server/lib/opencode/plugin-compatibility.js +15 -0
  168. package/server/lib/opencode/plugin-compatibility.test.js +21 -0
  169. package/server/lib/opencode/plugin-routes.js +3 -18
  170. package/server/lib/opencode/plugin-routes.test.js +9 -9
  171. package/server/lib/opencode/plugins.js +4 -3
  172. package/server/lib/opencode/plugins.test.js +12 -0
  173. package/server/lib/opencode/provider-catalog.js +58 -12
  174. package/server/lib/opencode/provider-catalog.test.js +48 -0
  175. package/server/lib/opencode/proxy.js +368 -28
  176. package/server/lib/opencode/proxy.test.js +338 -0
  177. package/server/lib/opencode/routes.js +303 -76
  178. package/server/lib/opencode/routes.test.js +315 -50
  179. package/server/lib/opencode/runtime-contract.js +401 -0
  180. package/server/lib/opencode/runtime-contract.test.js +214 -0
  181. package/server/lib/opencode/server-opencode-fetch.js +242 -0
  182. package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
  183. package/server/lib/opencode/server-utils-runtime.js +35 -0
  184. package/server/lib/opencode/shared-service.integration.test.js +66 -0
  185. package/server/lib/opencode/shared-service.js +147 -0
  186. package/server/lib/opencode/shared-service.test.js +93 -0
  187. package/server/lib/opencode/shared.js +3 -3
  188. package/server/lib/opencode/shutdown-runtime.js +15 -6
  189. package/server/lib/opencode/shutdown-runtime.test.js +49 -1
  190. package/server/lib/opencode/skill-routes.js +26 -40
  191. package/server/lib/opencode/skill-routes.test.js +71 -7
  192. package/server/lib/opencode/skills.test.js +15 -0
  193. package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
  194. package/server/lib/opencode/static-routes-runtime.js +3 -1
  195. package/server/lib/opencode/static-routes-runtime.test.js +33 -0
  196. package/server/lib/opencode/v1-migration-gate.js +157 -0
  197. package/server/lib/opencode/v1-migration-gate.test.js +111 -0
  198. package/server/lib/opencode/v2-client.js +60 -0
  199. package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
  200. package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
  201. package/server/lib/opencode/watcher.js +2 -1
  202. package/server/lib/opencode/watcher.test.js +1 -1
  203. package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
  204. package/server/lib/permission-auto-accept/runtime.js +14 -7
  205. package/server/lib/permission-auto-accept/runtime.test.js +27 -12
  206. package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
  207. package/server/lib/scheduled-tasks/DOCUMENTATION.md +79 -39
  208. package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
  209. package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
  210. package/server/lib/scheduled-tasks/routes.js +27 -0
  211. package/server/lib/scheduled-tasks/routes.test.js +94 -0
  212. package/server/lib/scheduled-tasks/runtime.js +381 -310
  213. package/server/lib/scheduled-tasks/runtime.test.js +600 -309
  214. package/server/lib/session-goal/DOCUMENTATION.md +173 -22
  215. package/server/lib/session-goal/capability.js +57 -0
  216. package/server/lib/session-goal/capability.test.js +33 -0
  217. package/server/lib/session-goal/routes.js +240 -2
  218. package/server/lib/session-goal/routes.test.js +211 -0
  219. package/server/lib/session-goal/runtime.js +1617 -113
  220. package/server/lib/session-goal/runtime.test.js +1500 -70
  221. package/server/lib/session-index/DOCUMENTATION.md +18 -15
  222. package/server/lib/session-index/event-ingest.js +89 -16
  223. package/server/lib/session-index/event-ingest.test.js +73 -0
  224. package/server/lib/session-index/host-authority-snapshot.js +92 -0
  225. package/server/lib/session-index/routes.host-archive.test.js +201 -0
  226. package/server/lib/session-index/routes.js +63 -10
  227. package/server/lib/session-index/routes.test.js +8 -8
  228. package/server/lib/session-index/service.js +7 -1
  229. package/server/lib/session-index/sync-runtime.js +171 -41
  230. package/server/lib/session-index/sync-runtime.test.js +164 -9
  231. package/server/lib/session-metadata/DOCUMENTATION.md +86 -0
  232. package/server/lib/session-metadata/routes.js +193 -0
  233. package/server/lib/session-metadata/routes.test.js +156 -0
  234. package/server/lib/session-metadata/session-archive.d.ts +73 -0
  235. package/server/lib/session-metadata/session-archive.js +654 -0
  236. package/server/lib/session-metadata/session-archive.test.js +359 -0
  237. package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
  238. package/server/lib/session-metadata/session-metadata-store.js +375 -0
  239. package/server/lib/session-metadata/session-metadata-store.test.js +379 -0
  240. package/server/lib/session-metadata/session-projection.d.ts +41 -0
  241. package/server/lib/session-metadata/session-projection.js +255 -0
  242. package/server/lib/session-metadata/session-projection.test.js +64 -0
  243. package/server/lib/session-metadata/system-session.js +55 -0
  244. package/server/lib/session-metadata/system-session.test.js +55 -0
  245. package/server/lib/session-title/DOCUMENTATION.md +40 -7
  246. package/server/lib/session-title/native-runtime.test.js +167 -0
  247. package/server/lib/session-title/runtime.js +67 -42
  248. package/server/lib/session-title/session-access.js +46 -0
  249. package/server/lib/session-title/session-access.test.js +91 -0
  250. package/server/lib/session-turn-pages/DOCUMENTATION.md +12 -11
  251. package/server/lib/session-turn-pages/reconcile.service.js +1 -1
  252. package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
  253. package/server/lib/session-turn-pages/routes.js +123 -75
  254. package/server/lib/session-turn-pages/routes.test.js +566 -0
  255. package/server/lib/session-turn-pages/service.js +6 -2
  256. package/server/lib/session-turn-pages/service.test.js +34 -0
  257. package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
  258. package/server/lib/session-turn-pages/session-message-projection.js +281 -0
  259. package/server/lib/small-model/DOCUMENTATION.md +101 -146
  260. package/server/lib/small-model/catalog.js +84 -54
  261. package/server/lib/small-model/catalog.test.js +111 -9
  262. package/server/lib/small-model/custom-api.js +68 -0
  263. package/server/lib/small-model/custom-api.test.js +64 -0
  264. package/server/lib/small-model/index.js +125 -241
  265. package/server/lib/small-model/resolve.js +35 -120
  266. package/server/lib/small-model/resolve.test.js +20 -110
  267. package/server/lib/small-model/routes.js +1 -3
  268. package/server/lib/small-model/summary-settings.test.js +157 -253
  269. package/server/opencode-proxy.test.js +63 -12
  270. package/dist/assets/AssistantView-BUveAgc4.js +0 -3
  271. package/dist/assets/MarkdownRenderer-BbrbDH6-.js +0 -2
  272. package/dist/assets/MobileDetailNavigation-B0aqKXd7.js +0 -1
  273. package/dist/assets/MobileSurface-DW7WfPsy.js +0 -2
  274. package/dist/assets/OverlayScrollbar-DrA0Vsg5.js +0 -2
  275. package/dist/assets/SettingsView-PkXIavr7.js +0 -25
  276. package/dist/assets/appThemeRegistry-D5Yxg4YF.js +0 -1
  277. package/dist/assets/collapsible-D5Phaykh.js +0 -1
  278. package/dist/assets/context-menu-CubSDW2g.js +0 -1
  279. package/dist/assets/es-DA3MqOo0.js +0 -15
  280. package/dist/assets/fr-Dbz0CdIA.js +0 -13
  281. package/dist/assets/gitApi-DXyC4MiV.js +0 -7
  282. package/dist/assets/insertionBoundaries-KlOVrHHt.js +0 -1
  283. package/dist/assets/ja-DumanVQA.js +0 -15
  284. package/dist/assets/ko-C0IkiUjQ.js +0 -15
  285. package/dist/assets/main-BPS5ylib.js +0 -2
  286. package/dist/assets/main-Cpd9VWYs.js +0 -169
  287. package/dist/assets/markdown-worker-CptoICmq.js +0 -2
  288. package/dist/assets/miniChat-B8_KLWiY.js +0 -2
  289. package/dist/assets/mobile-CtktKrXn.js +0 -2
  290. package/dist/assets/multirun-CYfP9Cfm.js +0 -4
  291. package/dist/assets/pl-B9No6MQp.js +0 -15
  292. package/dist/assets/platform-0w_Q9_ji.js +0 -2
  293. package/dist/assets/projectMeta-S1hJ7oBp.js +0 -3
  294. package/dist/assets/pt-BR-DyQ0WTDh.js +0 -15
  295. package/dist/assets/renderElectronMiniChatApp-iK14LWm8.js +0 -2
  296. package/dist/assets/renderMobileApp-DORLPWnb.js +0 -28
  297. package/dist/assets/src-rS3zCcsC.css +0 -2
  298. package/dist/assets/store-5B_LDnLM.js +0 -15
  299. package/dist/assets/uk-C_1KkqyP.js +0 -15
  300. package/dist/assets/useDesktopWindowControlsLayout-Dnr-jX0C.js +0 -1
  301. package/dist/assets/useEffectiveDirectory-CZLLyarc.js +0 -1
  302. package/dist/assets/useMobileNavigationStore-CoCiqNgm.js +0 -413
  303. package/dist/assets/useSessionAutoCleanup-xQyETQkG.js +0 -2
  304. package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
  305. package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
  306. package/dist/assets/zh-CN-uWILgBdt.js +0 -15
  307. package/dist/assets/zh-TW-MYfdvhbD.js +0 -15
  308. package/server/lib/small-model/call.js +0 -427
  309. package/server/lib/small-model/call.test.js +0 -382
  310. package/server/lib/small-model/opencode-session.js +0 -339
  311. package/server/lib/small-model/opencode-session.test.js +0 -177
@@ -1,6 +1,11 @@
1
1
  import { spawn, spawnSync } from 'node:child_process';
2
2
  import net from 'node:net';
3
3
  import { registerManagedProcess, unregisterManagedProcess, reapOrphanedProcesses } from './managed-process-registry.js';
4
+ import { evaluateOpenCodeHealthBody } from './opencode2-pin.js';
5
+ import { readOpenCode2BinaryVersion } from './ensure-cli.js';
6
+ import { createRevokedRuntimeContract, evaluateRuntimeContract } from './runtime-contract.js';
7
+ import { OPENCODE_V1_MIGRATION_PATH, fetchV1MigrationGate } from './v1-migration-gate.js';
8
+ import { isProcessAlive } from './shared-service.js';
4
9
 
5
10
  const parsePositiveInt = (value, fallback) => {
6
11
  const parsed = Number.parseInt(String(value ?? ''), 10);
@@ -14,7 +19,18 @@ const HEALTH_CHECK_MAX_CONSECUTIVE_FAILURES = parsePositiveInt(
14
19
  );
15
20
  const HEALTH_CHECK_INTERVAL_OVERRIDE_MS = parsePositiveInt(process.env.OPENCHAMBER_OPENCODE_HEALTH_INTERVAL_MS, 0);
16
21
  const HEALTH_CHECK_RESULT_CACHE_MS = parsePositiveInt(process.env.OPENCHAMBER_OPENCODE_HEALTH_CACHE_MS, 750);
17
- const OPENCODE_HEALTH_PATH = '/global/health';
22
+ // Official OpenCode 2.x exposes ServerInfo at GET /api/info (client.server.info).
23
+ const OPENCODE_HEALTH_PATH = '/api/info';
24
+ const OPENCODE_HEALTH_FALLBACK_PATH = '/global/health';
25
+ const DEFAULT_OPENCODE_SERVE_PORT = 4096;
26
+ // Polite SIGTERM window before SIGKILL for managed children (restart/OAuth/reload
27
+ // need enough time for clean flush; do not shorten globally for Quit latency).
28
+ const OPENCODE_CHILD_SIGTERM_GRACE_MS = 2500;
29
+ const OPENCODE_CHILD_SIGKILL_WAIT_MS = 1000;
30
+ // Last-used directory plus recently opened projects — deeper tails are unlikely
31
+ // to be the user's first click and just add background work.
32
+ const WARMUP_DIRECTORY_LIMIT = 4;
33
+ const WARMUP_REQUEST_TIMEOUT_MS = 30000;
18
34
 
19
35
  export const createOpenCodeLifecycleRuntime = (deps) => {
20
36
  const {
@@ -28,6 +44,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
28
44
  normalizeApiPrefix,
29
45
  applyOpencodeBinaryFromSettings,
30
46
  ensureOpencodeCliEnv,
47
+ ensurePinnedOpenCode2CliEnv,
31
48
  ensureLocalOpenCodeServerPassword,
32
49
  resolveManagedOpenCodeLaunchSpec,
33
50
  setOpenCodePort,
@@ -40,23 +57,51 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
40
57
  getManagedOpenCodeShellEnvSnapshot,
41
58
  managedCapabilitiesRuntime = null,
42
59
  getActiveSessionCount = () => 0,
60
+ getWarmupDirectories = async () => [],
61
+ // Official background service (shared-service.js). null keeps the private
62
+ // managed `opencode serve` path.
63
+ sharedService = null,
64
+ adoptOpenCodeServerPassword = null,
43
65
  } = deps;
44
66
 
45
- const killProcessOnPort = (port) => {
46
- if (!port || process.platform === 'win32') return;
47
- try {
48
- const result = spawnSync('lsof', ['-ti', `:${port}`], { encoding: 'utf8', timeout: 5000, windowsHide: true });
49
- const output = result.stdout || '';
50
- const myPid = process.pid;
51
- for (const pidStr of output.split(/\s+/)) {
52
- const pid = parseInt(pidStr.trim(), 10);
53
- if (pid && pid !== myPid) {
54
- try {
55
- spawnSync('kill', ['-9', String(pid)], { stdio: 'ignore', timeout: 2000 });
56
- } catch {
57
- }
58
- }
67
+ // Shared services outlive this runtime. Recovery re-ensures them; only an
68
+ // explicit binary upgrade replaces them through the official service CLI.
69
+ let sharedServiceAttached = false;
70
+ let sharedServicePid = null;
71
+
72
+ /**
73
+ * Force-signal only an owned managed OpenCode pid / process group.
74
+ * Never lsof-kill by port: that can murder clients still connected to the port
75
+ * or a newly bound external/shared `opencode serve` that we do not own.
76
+ * `port` is accepted for call-site compatibility and diagnostics only.
77
+ */
78
+ const killProcessOnPort = (port, ownedPid = null) => {
79
+ const pid = Number(ownedPid);
80
+ if (!Number.isInteger(pid) || pid <= 0) {
81
+ return;
82
+ }
83
+ if (pid === process.pid) {
84
+ return;
85
+ }
86
+
87
+ if (process.platform === 'win32') {
88
+ try {
89
+ spawnSync('taskkill', ['/pid', String(pid), '/f', '/t'], {
90
+ stdio: 'ignore',
91
+ timeout: 5000,
92
+ windowsHide: true,
93
+ });
94
+ } catch {
59
95
  }
96
+ return;
97
+ }
98
+
99
+ try {
100
+ process.kill(-pid, 'SIGKILL');
101
+ } catch {
102
+ }
103
+ try {
104
+ process.kill(pid, 'SIGKILL');
60
105
  } catch {
61
106
  }
62
107
  };
@@ -205,13 +250,13 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
205
250
 
206
251
  signalProcessTree('SIGTERM');
207
252
 
208
- if (await waitForChildProcessClose(child, 2500)) {
253
+ if (await waitForChildProcessClose(child, OPENCODE_CHILD_SIGTERM_GRACE_MS)) {
209
254
  return;
210
255
  }
211
256
 
212
257
  signalProcessTree('SIGKILL');
213
258
 
214
- await waitForChildProcessClose(child, 1000);
259
+ await waitForChildProcessClose(child, OPENCODE_CHILD_SIGKILL_WAIT_MS);
215
260
  };
216
261
 
217
262
  const closeManagedOpenCodeChild = async (child) => {
@@ -306,8 +351,11 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
306
351
  stdout += chunk.toString();
307
352
  const lines = stdout.split('\n');
308
353
  for (const line of lines) {
309
- if (!line.startsWith('opencode server listening')) continue;
310
- const match = line.match(/on\s+(https?:\/\/[^\s]+)/);
354
+ const trimmed = line.trim();
355
+ // v2 prints `server listening on http://...` without the `opencode `
356
+ // prefix; 1.x still prints `opencode server listening on ...`.
357
+ if (!trimmed.startsWith('opencode server listening') && !trimmed.startsWith('server listening')) continue;
358
+ const match = trimmed.match(/on\s+(https?:\/\/[^\s]+)/);
311
359
  if (!match) {
312
360
  finish(reject, new Error(`Failed to parse server url from output: ${line}`));
313
361
  return;
@@ -403,23 +451,164 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
403
451
  });
404
452
  };
405
453
 
454
+ // Official v2 only publishes GET /api/experimental/migration/v1. Backfill
455
+ // runs inside opencode2; we poll status and never POST.
456
+ const probeV1MigrationGate = async (signal) => fetchV1MigrationGate({
457
+ url: buildOpenCodeUrl(OPENCODE_V1_MIGRATION_PATH, ''),
458
+ headers: getOpenCodeAuthHeaders(),
459
+ signal,
460
+ });
461
+
462
+ // Health ok is not enough to pull session messages. required/running/error
463
+ // keep isOpenCodeReady false so the proxy ready gate still blocks transcript.
464
+ const applyV1MigrationGate = (gate) => {
465
+ state.v1Migration = gate;
466
+ if (gate.admitTranscript) {
467
+ state.isOpenCodeReady = true;
468
+ state.openCodeNotReadySince = 0;
469
+ return true;
470
+ }
471
+ state.isOpenCodeReady = false;
472
+ if (!state.openCodeNotReadySince) {
473
+ state.openCodeNotReadySince = Date.now();
474
+ }
475
+ if (gate.phase === 'error' && gate.error) {
476
+ state.lastOpenCodeError = gate.error;
477
+ }
478
+ return false;
479
+ };
480
+
481
+ // v2 readiness lives at /api/info (ServerInfo.version); /global/health remains
482
+ // a probe fallback for older sidecars. Both require Basic auth from
483
+ // getOpenCodeAuthHeaders(). Version admission rejects 1.x / missing / noise.
484
+ // Health ok is reachability only — execution semantics live on runtimeContract.
485
+ const fetchOpenCodeHealthResult = async (urlForPath, signal, options = {}) => {
486
+ const headerCandidates = [
487
+ { Accept: 'application/json', ...getOpenCodeAuthHeaders(), authenticated: true },
488
+ ];
489
+ if (options.allowUnauthenticated) {
490
+ headerCandidates.push({ Accept: 'application/json', authenticated: false });
491
+ }
492
+ let sawUnauthorized = false;
493
+ let lastGate = null;
494
+ for (const headerCandidate of headerCandidates) {
495
+ const { authenticated, ...headers } = headerCandidate;
496
+ for (const healthPath of [OPENCODE_HEALTH_PATH, OPENCODE_HEALTH_FALLBACK_PATH]) {
497
+ try {
498
+ const response = await fetch(urlForPath(healthPath), {
499
+ method: 'GET',
500
+ headers,
501
+ signal,
502
+ });
503
+ if (response.status === 401 || response.status === 403) {
504
+ sawUnauthorized = true;
505
+ continue;
506
+ }
507
+ if (!response.ok) continue;
508
+ const body = await response.json().catch(() => null);
509
+ const gate = evaluateOpenCodeHealthBody(body);
510
+ lastGate = gate;
511
+ if (gate.ok) {
512
+ return {
513
+ ok: true,
514
+ version: gate.version,
515
+ authenticated: authenticated !== false,
516
+ healthOk: true,
517
+ };
518
+ }
519
+ } catch {
520
+ }
521
+ }
522
+ }
523
+ if (sawUnauthorized && !lastGate?.ok) {
524
+ return { ok: false, version: null, authenticated: false, healthOk: false, reason: 'auth-failed' };
525
+ }
526
+ return {
527
+ ok: false,
528
+ version: lastGate?.version ?? null,
529
+ authenticated: sawUnauthorized ? false : null,
530
+ healthOk: false,
531
+ reason: lastGate?.reason || 'unhealthy',
532
+ };
533
+ };
534
+
535
+ const fetchOpenCodeHealthOk = async (urlForPath, signal, options = {}) => {
536
+ const result = await fetchOpenCodeHealthResult(urlForPath, signal, options);
537
+ return result.ok === true;
538
+ };
539
+
540
+ /**
541
+ * Invalidate any prior execution permit. Start / restart / target changes must
542
+ * bump generation so in-flight async health probes cannot publish for a newer
543
+ * instance, and Host/proxy gates stop holding a stale `executionAllowed: true`.
544
+ * @param {string} [reason]
545
+ */
546
+ const revokeRuntimeExecutionPermit = (reason = 'instance-revoked') => {
547
+ const nextGeneration = (Number(state.runtimeContractGeneration) || 0) + 1;
548
+ state.runtimeContractGeneration = nextGeneration;
549
+ state.openCodeServeVersion = null;
550
+ state.openCodeCliVersion = null;
551
+ state.runtimeContract = createRevokedRuntimeContract(reason, {
552
+ instanceGeneration: nextGeneration,
553
+ });
554
+ return nextGeneration;
555
+ };
556
+
557
+ /**
558
+ * Publish contract only for the generation that started the probe.
559
+ * @param {object | null | undefined} probe
560
+ * @param {object | null | undefined} migrationGate
561
+ * @param {number | null | undefined} expectedGeneration
562
+ */
563
+ const refreshRuntimeContractFromProbe = (
564
+ probe,
565
+ migrationGate = state.v1Migration,
566
+ expectedGeneration = state.runtimeContractGeneration,
567
+ ) => {
568
+ const currentGeneration = Number(state.runtimeContractGeneration) || 0;
569
+ if (
570
+ expectedGeneration != null
571
+ && Number(expectedGeneration) !== currentGeneration
572
+ ) {
573
+ // Stale async health / migration result for a superseded instance.
574
+ return state.runtimeContract;
575
+ }
576
+ const binaryPath = typeof process.env.OPENCODE_BINARY === 'string'
577
+ ? process.env.OPENCODE_BINARY.trim()
578
+ : '';
579
+ const cliVersion = binaryPath ? (readOpenCode2BinaryVersion(binaryPath) || null) : null;
580
+ const contract = evaluateRuntimeContract({
581
+ serveVersion: probe?.version ?? null,
582
+ cliVersion,
583
+ reachable: probe?.ok === true || Boolean(state.openCodePort),
584
+ authenticated: probe?.authenticated ?? null,
585
+ healthOk: probe?.healthOk ?? probe?.ok ?? null,
586
+ migrationAdmitTranscript: migrationGate?.admitTranscript ?? null,
587
+ migrationPhase: migrationGate?.phase ?? null,
588
+ migrationError: migrationGate?.error ?? null,
589
+ });
590
+ contract.instanceGeneration = currentGeneration;
591
+ state.openCodeServeVersion = contract.serveVersion;
592
+ state.openCodeCliVersion = contract.cliVersion;
593
+ state.runtimeContract = contract;
594
+ return contract;
595
+ };
596
+
406
597
  const isOpenCodeProcessHealthy = async () => {
407
- if (!state.openCodeProcess || !state.openCodePort) {
598
+ if ((!state.openCodeProcess && !sharedServiceAttached) || !state.openCodePort) {
408
599
  return false;
409
600
  }
410
601
 
602
+ const generationAtStart = Number(state.runtimeContractGeneration) || 0;
411
603
  try {
412
- const response = await fetch(buildOpenCodeUrl(OPENCODE_HEALTH_PATH, ''), {
413
- method: 'GET',
414
- headers: {
415
- Accept: 'application/json',
416
- ...getOpenCodeAuthHeaders(),
417
- },
418
- signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS),
419
- });
420
- if (!response.ok) return false;
421
- const body = await response.json().catch(() => null);
422
- return body?.healthy === true;
604
+ const result = await fetchOpenCodeHealthResult(
605
+ (healthPath) => buildOpenCodeUrl(healthPath, ''),
606
+ AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS)
607
+ );
608
+ if (result.ok) {
609
+ refreshRuntimeContractFromProbe(result, state.v1Migration, generationAtStart);
610
+ }
611
+ return result.ok;
423
612
  } catch {
424
613
  return false;
425
614
  }
@@ -430,22 +619,21 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
430
619
  return false;
431
620
  }
432
621
 
622
+ const generationAtStart = Number(state.runtimeContractGeneration) || 0;
433
623
  try {
434
624
  const controller = new AbortController();
435
625
  const timeout = setTimeout(() => controller.abort(), 3000);
436
626
  const base = origin ?? `http://127.0.0.1:${port}`;
437
- const response = await fetch(`${base}${OPENCODE_HEALTH_PATH}`, {
438
- method: 'GET',
439
- headers: {
440
- Accept: 'application/json',
441
- ...getOpenCodeAuthHeaders(),
442
- },
443
- signal: controller.signal,
444
- });
627
+ const result = await fetchOpenCodeHealthResult(
628
+ (healthPath) => `${base}${healthPath}`,
629
+ controller.signal,
630
+ { allowUnauthenticated: true },
631
+ );
445
632
  clearTimeout(timeout);
446
- if (!response.ok) return false;
447
- const body = await response.json().catch(() => null);
448
- return body?.healthy === true;
633
+ if (result.ok) {
634
+ refreshRuntimeContractFromProbe(result, state.v1Migration, generationAtStart);
635
+ }
636
+ return result.ok;
449
637
  } catch {
450
638
  return false;
451
639
  }
@@ -467,12 +655,120 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
467
655
  throw new Error('Timed out waiting for OpenCode port');
468
656
  };
469
657
 
658
+ /**
659
+ * Ensure the official service is running and point this runtime at its
660
+ * registration (origin, port, password). Throws when the CLI cannot start it
661
+ * or the registration is missing/malformed; the caller decides rollback.
662
+ */
663
+ const connectSharedOpenCodeService = async ({ binaryPath } = {}) => {
664
+ if (!binaryPath) {
665
+ await applyOpencodeBinaryFromSettings({ strict: true });
666
+ if (typeof ensurePinnedOpenCode2CliEnv === 'function') {
667
+ await ensurePinnedOpenCode2CliEnv();
668
+ }
669
+ ensureOpencodeCliEnv();
670
+ }
671
+ let binary = binaryPath || (process.env.OPENCODE_BINARY || 'opencode').trim() || 'opencode';
672
+ let args = [];
673
+ if (process.platform === 'win32') {
674
+ const launchSpec = resolveManagedOpenCodeLaunchSpec(binary);
675
+ if (launchSpec?.binary) {
676
+ binary = launchSpec.binary;
677
+ args = Array.isArray(launchSpec.args) ? launchSpec.args : [];
678
+ }
679
+ }
680
+ const envPath = typeof buildManagedOpenCodePath === 'function'
681
+ ? buildManagedOpenCodePath()
682
+ : typeof buildAugmentedPath === 'function'
683
+ ? buildAugmentedPath()
684
+ : process.env.PATH;
685
+ const shellEnv = typeof getManagedOpenCodeShellEnvSnapshot === 'function'
686
+ ? getManagedOpenCodeShellEnvSnapshot() || {}
687
+ : {};
688
+ await sharedService.start({
689
+ binary,
690
+ args,
691
+ replace: Boolean(binaryPath),
692
+ env: sharedService.buildStartEnv({ ...shellEnv, ...process.env, PATH: envPath }),
693
+ });
694
+ const registration = await sharedService.readRegistration();
695
+ if (!registration) {
696
+ throw new Error(`OpenCode service started but ${sharedService.registrationFile} is missing or invalid`);
697
+ }
698
+ adoptOpenCodeServerPassword?.(registration.password, 'shared-service');
699
+ state.openCodeBaseUrl = registration.origin;
700
+ setOpenCodePort(registration.port);
701
+ sharedServicePid = registration.pid;
702
+ return registration;
703
+ };
704
+
705
+ /**
706
+ * Attach to the shared official service instead of spawning a private serve.
707
+ * Failure leaves integration unavailable and retryable, never silently
708
+ * starting a private server with different lifecycle semantics.
709
+ */
710
+ const attachSharedOpenCodeService = async () => {
711
+ if (!sharedService) return false;
712
+ revokeRuntimeExecutionPermit('shared-service-attach');
713
+ state.isOpenCodeReady = false;
714
+ state.openCodeNotReadySince = Date.now();
715
+ try {
716
+ const registration = await connectSharedOpenCodeService();
717
+ if (!(await probeExternalOpenCode(registration.port, registration.origin))) {
718
+ throw new Error(`OpenCode service at ${registration.origin} did not pass the health/version check`);
719
+ }
720
+ if (state.openCodeProcess && !state.isExternalOpenCode) {
721
+ await state.openCodeProcess.close();
722
+ state.openCodeProcess = null;
723
+ }
724
+ sharedServiceAttached = true;
725
+ state.isExternalOpenCode = true;
726
+ state.lastOpenCodeError = null;
727
+ state.v1Migration = null;
728
+ state.managedCapabilityIdentity = null;
729
+ managedCapabilitiesRuntime?.setCapabilityIdentity(null);
730
+ syncToHmrState();
731
+ console.log(`[OpenCode] Attached to shared OpenCode service at ${registration.origin} (v${registration.version ?? 'unknown'})`);
732
+ return true;
733
+ } catch (error) {
734
+ sharedServiceAttached = false;
735
+ sharedServicePid = null;
736
+ state.openCodeBaseUrl = null;
737
+ state.openCodePort = null;
738
+ adoptOpenCodeServerPassword?.(null, null);
739
+ syncToHmrState();
740
+ throw error;
741
+ }
742
+ };
743
+
744
+ // `location.reload` rebuilds every loaded location in place: running
745
+ // sessions continue at the next step boundary and clients get
746
+ // `location.shutdown` to refetch location services.
747
+ const reloadSharedOpenCodeLocations = async () => {
748
+ const response = await fetch(buildOpenCodeUrl('/api/location/reload', ''), {
749
+ method: 'POST',
750
+ headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
751
+ signal: AbortSignal.timeout(120_000),
752
+ });
753
+ if (!response.ok) {
754
+ const body = await response.text().catch(() => '');
755
+ throw new Error(`OpenCode location reload failed (HTTP ${response.status})${body ? `: ${body.slice(0, 300)}` : ''}`);
756
+ }
757
+ };
758
+
470
759
  const START_OPEN_CODE_MAX_ATTEMPTS = 2;
471
760
 
472
761
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
473
762
 
474
763
  const startOpenCodeOnce = async () => {
475
- const desiredPort = env.ENV_CONFIGURED_OPENCODE_PORT ?? 0;
764
+ // New managed instance — drop any prior permit before spawn/probes.
765
+ const generationAtStart = revokeRuntimeExecutionPermit('managed-start');
766
+ const requestedPort = env.ENV_CONFIGURED_OPENCODE_PORT;
767
+ const defaultPortFree = requestedPort
768
+ ? false
769
+ : await waitForPortRelease(DEFAULT_OPENCODE_SERVE_PORT, 200, env.ENV_CONFIGURED_OPENCODE_HOSTNAME);
770
+ const desiredPort = requestedPort
771
+ ?? (defaultPortFree ? DEFAULT_OPENCODE_SERVE_PORT : 0);
476
772
  const spawnPort = await resolveManagedOpenCodePort(desiredPort, env.ENV_CONFIGURED_OPENCODE_HOSTNAME);
477
773
  console.log(
478
774
  desiredPort > 0
@@ -481,6 +777,9 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
481
777
  );
482
778
 
483
779
  await applyOpencodeBinaryFromSettings({ strict: true });
780
+ if (typeof ensurePinnedOpenCode2CliEnv === 'function') {
781
+ await ensurePinnedOpenCode2CliEnv();
782
+ }
484
783
  ensureOpencodeCliEnv();
485
784
  const openCodePassword = await ensureLocalOpenCodeServerPassword({ rotateManaged: true });
486
785
  const envPath = typeof buildManagedOpenCodePath === 'function'
@@ -523,9 +822,17 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
523
822
  setOpenCodePort(port);
524
823
  setDetectedOpenCodeApiPrefix(prefix);
525
824
 
526
- state.isOpenCodeReady = true;
527
- state.lastOpenCodeError = null;
528
- state.openCodeNotReadySince = 0;
825
+ // Evaluate contract for THIS generation from live health + migration.
826
+ const healthResult = await fetchOpenCodeHealthResult(
827
+ (healthPath) => buildOpenCodeUrl(healthPath, ''),
828
+ AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS),
829
+ );
830
+ const gate = await probeV1MigrationGate(AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS));
831
+ refreshRuntimeContractFromProbe(healthResult, gate, generationAtStart);
832
+ if (applyV1MigrationGate(gate)) {
833
+ refreshRuntimeContractFromProbe(healthResult, gate, generationAtStart);
834
+ state.lastOpenCodeError = null;
835
+ }
529
836
 
530
837
  return serverInstance;
531
838
  }
@@ -539,6 +846,14 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
539
846
  const message = error instanceof Error ? error.message : String(error);
540
847
  state.lastOpenCodeError = message;
541
848
  state.openCodePort = null;
849
+ // Keep revoked/pending contract — do not resurrect the prior permit.
850
+ if ((Number(state.runtimeContractGeneration) || 0) === generationAtStart) {
851
+ refreshRuntimeContractFromProbe(
852
+ { ok: false, version: null, authenticated: null, healthOk: false },
853
+ state.v1Migration,
854
+ generationAtStart,
855
+ );
856
+ }
542
857
  syncToHmrState();
543
858
  console.error(`Failed to start OpenCode: ${message}`);
544
859
  throw error;
@@ -572,20 +887,35 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
572
887
  throw lastError;
573
888
  };
574
889
 
575
- const restartOpenCode = async () => {
890
+ const restartOpenCode = async (options = {}) => {
576
891
  if (state.isShuttingDown) return;
577
892
  if (state.currentRestartPromise) {
578
893
  await state.currentRestartPromise;
579
- return;
894
+ if (!options.binaryPath) return;
580
895
  }
581
896
 
582
897
  state.currentRestartPromise = (async () => {
583
898
  state.isRestartingOpenCode = true;
584
899
  state.isOpenCodeReady = false;
585
900
  state.openCodeNotReadySince = Date.now();
901
+ // Drop prior instance permit before any probe / respawn.
902
+ const restartGeneration = revokeRuntimeExecutionPermit('managed-restart');
586
903
  console.log('Restarting OpenCode process...');
587
904
 
588
905
  if (state.isExternalOpenCode) {
906
+ if (sharedServiceAttached) {
907
+ // The service may have been stopped or replaced by another client
908
+ // (new pid, possibly new port/password); recovery only rediscovers it.
909
+ // An explicit upgrade supplies a verified owned-cache binary.
910
+ console.log('Re-ensuring shared OpenCode service...');
911
+ try {
912
+ await connectSharedOpenCodeService(options);
913
+ } catch (error) {
914
+ state.lastOpenCodeError = error instanceof Error ? error.message : String(error);
915
+ syncToHmrState();
916
+ throw error;
917
+ }
918
+ }
589
919
  console.log('Re-probing external OpenCode server...');
590
920
  const probePort = state.openCodePort || env.ENV_CONFIGURED_OPENCODE_PORT || 4096;
591
921
  const probeOrigin = state.openCodeBaseUrl ?? env.ENV_CONFIGURED_OPENCODE_HOST?.origin;
@@ -593,12 +923,20 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
593
923
  if (healthy) {
594
924
  console.log(`External OpenCode server on port ${probePort} is healthy`);
595
925
  setOpenCodePort(probePort);
596
- state.isOpenCodeReady = true;
597
- state.lastOpenCodeError = null;
598
- state.openCodeNotReadySince = 0;
926
+ const healthResult = await fetchOpenCodeHealthResult(
927
+ (healthPath) => buildOpenCodeUrl(healthPath, ''),
928
+ AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS),
929
+ );
930
+ const gate = await probeV1MigrationGate(AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS));
931
+ refreshRuntimeContractFromProbe(healthResult, gate, restartGeneration);
932
+ if (applyV1MigrationGate(gate)) {
933
+ refreshRuntimeContractFromProbe(healthResult, gate, restartGeneration);
934
+ state.lastOpenCodeError = null;
935
+ }
599
936
  syncToHmrState();
600
937
  } else {
601
938
  state.lastOpenCodeError = `External OpenCode server on port ${probePort} is not responding`;
939
+ state.v1Migration = null;
602
940
  console.error(state.lastOpenCodeError);
603
941
  throw new Error(state.lastOpenCodeError);
604
942
  }
@@ -610,11 +948,13 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
610
948
  return;
611
949
  }
612
950
 
613
- const portToKill = state.openCodePort;
951
+ const portToRelease = state.openCodePort;
614
952
 
615
953
  if (state.openCodeProcess) {
616
954
  console.log('Stopping existing OpenCode process...');
617
955
  try {
956
+ // close() owns the full SIGTERM→SIGKILL escalation on this child handle.
957
+ // Do not SIGKILL a pre-captured pid afterward — the OS may have recycled it.
618
958
  await state.openCodeProcess.close();
619
959
  } catch (error) {
620
960
  console.warn('Error closing OpenCode process:', error);
@@ -623,9 +963,9 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
623
963
  syncToHmrState();
624
964
  }
625
965
 
626
- killProcessOnPort(portToKill);
627
- if (!(await waitForPortRelease(portToKill, 5000))) {
628
- console.warn(`Timed out waiting for OpenCode port ${portToKill} to be released`);
966
+ // Probe only: never mass-kill whoever holds the port (clients / external serve).
967
+ if (!(await waitForPortRelease(portToRelease, 5000))) {
968
+ console.warn(`Timed out waiting for OpenCode port ${portToRelease} to be released`);
629
969
  }
630
970
 
631
971
  if (env.ENV_CONFIGURED_OPENCODE_PORT) {
@@ -658,12 +998,16 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
658
998
  } catch (error) {
659
999
  console.error(`Failed to restart OpenCode: ${error.message}`);
660
1000
  state.lastOpenCodeError = error.message;
661
- if (!env.ENV_CONFIGURED_OPENCODE_PORT) {
662
- state.openCodePort = null;
663
- syncToHmrState();
1001
+ // External serve is read-only: never clear its port, flip ownership, or
1002
+ // spawn a managed replacement when a re-probe fails.
1003
+ if (!state.isExternalOpenCode) {
1004
+ if (!env.ENV_CONFIGURED_OPENCODE_PORT) {
1005
+ state.openCodePort = null;
1006
+ syncToHmrState();
1007
+ }
1008
+ state.openCodeApiPrefixDetected = true;
1009
+ state.openCodeApiPrefix = '';
664
1010
  }
665
- state.openCodeApiPrefixDetected = true;
666
- state.openCodeApiPrefix = '';
667
1011
  throw error;
668
1012
  } finally {
669
1013
  state.currentRestartPromise = null;
@@ -678,36 +1022,49 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
678
1022
 
679
1023
  const deadline = Date.now() + timeoutMs;
680
1024
  let lastError = null;
1025
+ // Pin generation for this wait loop so a concurrent restart cannot have our
1026
+ // late probe overwrite the newer instance contract.
1027
+ const generationAtStart = Number(state.runtimeContractGeneration) || 0;
681
1028
 
682
1029
  while (Date.now() < deadline) {
683
1030
  let timeout = null;
684
1031
  try {
1032
+ if ((Number(state.runtimeContractGeneration) || 0) !== generationAtStart) {
1033
+ throw new Error('OpenCode instance changed while waiting for ready');
1034
+ }
1035
+ // One timer covers health + migration for this attempt. Clearing after
1036
+ // health alone let a hung migration outrun HEALTH_CHECK_TIMEOUT_MS and
1037
+ // the outer deadline when the probe never settled.
685
1038
  const controller = new AbortController();
686
1039
  timeout = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT_MS);
687
- const response = await fetch(buildOpenCodeUrl(OPENCODE_HEALTH_PATH, ''), {
688
- method: 'GET',
689
- headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
690
- signal: controller.signal,
691
- });
692
- clearTimeout(timeout);
693
- timeout = null;
1040
+ const healthResult = await fetchOpenCodeHealthResult(
1041
+ (healthPath) => buildOpenCodeUrl(healthPath, ''),
1042
+ controller.signal
1043
+ );
694
1044
 
695
- if (!response.ok) {
696
- lastError = new Error(`OpenCode health endpoint responded with status ${response.status}`);
1045
+ if (!healthResult.ok) {
1046
+ refreshRuntimeContractFromProbe(healthResult, state.v1Migration, generationAtStart);
1047
+ lastError = new Error(
1048
+ healthResult.reason === 'auth-failed'
1049
+ ? 'OpenCode health endpoint rejected authentication'
1050
+ : 'OpenCode health endpoint returned unhealthy response',
1051
+ );
697
1052
  await new Promise((resolve) => setTimeout(resolve, intervalMs));
698
1053
  continue;
699
1054
  }
700
1055
 
701
- const body = await response.json().catch(() => null);
702
- if (body?.healthy !== true) {
703
- lastError = new Error('OpenCode health endpoint returned unhealthy response');
704
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
705
- continue;
1056
+ const gate = await probeV1MigrationGate(controller.signal);
1057
+ refreshRuntimeContractFromProbe(healthResult, gate, generationAtStart);
1058
+ if (applyV1MigrationGate(gate)) {
1059
+ // Re-publish contract after admitTranscript flips ready.
1060
+ refreshRuntimeContractFromProbe(healthResult, gate, generationAtStart);
1061
+ state.lastOpenCodeError = null;
1062
+ return;
706
1063
  }
707
1064
 
708
- state.isOpenCodeReady = true;
709
- state.lastOpenCodeError = null;
710
- return;
1065
+ lastError = new Error(gate.error || `V1 migration is ${gate.phase}`);
1066
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
1067
+ continue;
711
1068
  } catch (error) {
712
1069
  lastError = error;
713
1070
  } finally {
@@ -761,6 +1118,16 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
761
1118
  const { agentName } = options;
762
1119
 
763
1120
  console.log(`Refreshing OpenCode after ${reason}`);
1121
+
1122
+ if (sharedServiceAttached) {
1123
+ // Shared with other clients: reload in place instead of restarting.
1124
+ await reloadSharedOpenCodeLocations();
1125
+ if (agentName) {
1126
+ await waitForAgentPresence(agentName);
1127
+ }
1128
+ return { reloaded: true, external: true };
1129
+ }
1130
+
764
1131
  clearResolvedOpenCodeBinary();
765
1132
  await applyOpencodeBinaryFromSettings();
766
1133
 
@@ -828,35 +1195,47 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
828
1195
  console.log(`Using external OpenCode server at ${label} (skip-start mode)`);
829
1196
  state.openCodeBaseUrl = env.ENV_CONFIGURED_OPENCODE_HOST?.origin ?? null;
830
1197
  setOpenCodePort(env.ENV_EFFECTIVE_PORT);
831
- state.isOpenCodeReady = true;
1198
+ // External attach is not ready until version + V1 migration admit.
1199
+ revokeRuntimeExecutionPermit('external-attach');
1200
+ state.isOpenCodeReady = false;
1201
+ state.openCodeNotReadySince = Date.now();
832
1202
  state.isExternalOpenCode = true;
833
1203
  state.lastOpenCodeError = null;
834
- state.openCodeNotReadySince = 0;
1204
+ state.v1Migration = null;
835
1205
  syncToHmrState();
1206
+ } else if (
1207
+ sharedService
1208
+ && !env.ENV_CONFIGURED_OPENCODE_PORT
1209
+ && !env.ENV_CONFIGURED_OPENCODE_HOST
1210
+ ) {
1211
+ await attachSharedOpenCodeService();
836
1212
  } else if (await isOpenCodeProcessHealthy() && (managedCapabilitiesRuntime ? managedCapabilitiesRuntime.hasValidIdentity() : true)) {
837
1213
  console.log(`[HMR] Reusing existing OpenCode process on port ${state.openCodePort}`);
838
1214
  } else if (state.openCodeProcess && !state.isExternalOpenCode && await isOpenCodeProcessHealthy()) {
839
1215
  console.log('[HMR] Restarting managed OpenCode because scheduled-task capability identity is stale');
840
1216
  await restartOpenCode();
841
- } else if (env.ENV_EFFECTIVE_PORT && await probeExternalOpenCode(env.ENV_EFFECTIVE_PORT, env.ENV_CONFIGURED_OPENCODE_HOST?.origin)) {
842
- const label = env.ENV_CONFIGURED_OPENCODE_HOST ? env.ENV_CONFIGURED_OPENCODE_HOST.origin : `http://localhost:${env.ENV_EFFECTIVE_PORT}`;
843
- console.log(`Auto-detected existing OpenCode server at ${label}`);
1217
+ } else if (
1218
+ (env.ENV_EFFECTIVE_PORT || DEFAULT_OPENCODE_SERVE_PORT)
1219
+ && await probeExternalOpenCode(
1220
+ env.ENV_EFFECTIVE_PORT || DEFAULT_OPENCODE_SERVE_PORT,
1221
+ env.ENV_CONFIGURED_OPENCODE_HOST?.origin,
1222
+ )
1223
+ ) {
1224
+ const reusePort = env.ENV_EFFECTIVE_PORT || DEFAULT_OPENCODE_SERVE_PORT;
1225
+ const label = env.ENV_CONFIGURED_OPENCODE_HOST ? env.ENV_CONFIGURED_OPENCODE_HOST.origin : `http://localhost:${reusePort}`;
1226
+ console.log(`Reusing existing OpenCode serve at ${label}`);
844
1227
  state.openCodeBaseUrl = env.ENV_CONFIGURED_OPENCODE_HOST?.origin ?? null;
845
- setOpenCodePort(env.ENV_EFFECTIVE_PORT);
846
- state.isOpenCodeReady = true;
1228
+ setOpenCodePort(reusePort);
1229
+ // Health ok is not transcript-ready; waitForOpenCodeReady applies the gate.
1230
+ state.isOpenCodeReady = false;
1231
+ state.openCodeNotReadySince = Date.now();
847
1232
  state.isExternalOpenCode = true;
848
1233
  state.lastOpenCodeError = null;
849
- state.openCodeNotReadySince = 0;
1234
+ state.v1Migration = null;
850
1235
  syncToHmrState();
851
1236
  } else {
852
- // We never auto-attach to an arbitrary pre-existing OpenCode instance.
853
- // Attaching to an external server requires explicit opt-in via env
854
- // (OPENCODE_HOST / OPENCODE_PORT / OPENCODE_SKIP_START), handled by the
855
- // branches above. Without that opt-in we always start our OWN managed
856
- // instance on a freshly-allocated port. A blind probe of the default
857
- // port 4096 used to hijack a user's separately-running OpenCode (e.g.
858
- // the OpenCode desktop app), coupling our lifecycle to theirs and
859
- // breaking init against an unexpected server version/config.
1237
+ // No healthy v2 serve on the requested/default port. Start a generic
1238
+ // `opencode serve` so OpenChamber and other OpenCode clients can share it.
860
1239
  state.isExternalOpenCode = false;
861
1240
  state.openCodeBaseUrl = null;
862
1241
  if (env.ENV_EFFECTIVE_PORT) {
@@ -874,6 +1253,11 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
874
1253
  await waitForOpenCodePort();
875
1254
  try {
876
1255
  await waitForOpenCodeReady();
1256
+ // Warm only after readiness admits; a failed gate must not kick off
1257
+ // directory-scoped traffic against an unready upstream.
1258
+ if (state.isOpenCodeReady) {
1259
+ void warmOpenCodeDirectories();
1260
+ }
877
1261
  } catch (error) {
878
1262
  console.error(`OpenCode readiness check failed: ${error.message}`);
879
1263
  }
@@ -884,6 +1268,47 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
884
1268
  }
885
1269
  };
886
1270
 
1271
+ // OpenCode initializes each project directory lazily on its first
1272
+ // directory-scoped request, and that initialization takes seconds on large
1273
+ // session stores. Without warming, the user's first session open pays it
1274
+ // interactively. Warm the most recently used directories right after
1275
+ // readiness so the work overlaps UI startup instead. Sequential and
1276
+ // best-effort: a failed or slow directory never blocks the others for long,
1277
+ // and a restart invalidates the pass via the port/readiness guard.
1278
+ const warmOpenCodeDirectories = async () => {
1279
+ let directories = [];
1280
+ try {
1281
+ directories = await getWarmupDirectories();
1282
+ } catch {
1283
+ return;
1284
+ }
1285
+ if (!Array.isArray(directories) || directories.length === 0) return;
1286
+
1287
+ const warmedPort = state.openCodePort;
1288
+ for (const directory of directories.slice(0, WARMUP_DIRECTORY_LIMIT)) {
1289
+ if (typeof directory !== 'string' || !directory) continue;
1290
+ if (!state.isOpenCodeReady || state.openCodePort !== warmedPort) return;
1291
+ let timeout = null;
1292
+ try {
1293
+ const controller = new AbortController();
1294
+ timeout = setTimeout(() => controller.abort(), WARMUP_REQUEST_TIMEOUT_MS);
1295
+ // Warming a directory is the point, not the answer: any directory-scoped
1296
+ // read makes OpenCode initialise it. `/api/session` is the cheapest one
1297
+ // that takes a directory.
1298
+ const url = `${buildOpenCodeUrl('/api/session', '')}?directory=${encodeURIComponent(directory)}&limit=1`;
1299
+ await fetch(url, {
1300
+ method: 'GET',
1301
+ headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
1302
+ signal: controller.signal,
1303
+ });
1304
+ } catch {
1305
+ // Best-effort — the directory stays lazy and the UI's own request warms it.
1306
+ } finally {
1307
+ if (timeout) clearTimeout(timeout);
1308
+ }
1309
+ }
1310
+ };
1311
+
887
1312
  const bootstrapOpenCodeAtStartup = () => {
888
1313
  if (!bootstrapOpenCodePromise) {
889
1314
  bootstrapOpenCodePromise = runBootstrapOpenCodeAtStartup().finally(() => {
@@ -903,6 +1328,9 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
903
1328
 
904
1329
  clearResolvedOpenCodeBinary();
905
1330
  await applyOpencodeBinaryFromSettings({ strict: true });
1331
+ if (typeof ensurePinnedOpenCode2CliEnv === 'function') {
1332
+ await ensurePinnedOpenCode2CliEnv();
1333
+ }
906
1334
  const resolved = ensureOpencodeCliEnv();
907
1335
  if (!resolved) {
908
1336
  const error = new Error('OpenCode CLI could not be resolved');
@@ -992,13 +1420,21 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
992
1420
  };
993
1421
 
994
1422
  const runHealthCheckCycle = async (source) => {
995
- if (!state.openCodeProcess || state.isShuttingDown || state.isRestartingOpenCode) return;
1423
+ // External serve is read-only mount: never restart or spawn a second copy
1424
+ // because the user's instance looks unhealthy from our probe. The shared
1425
+ // service is the exception: "restart" only re-runs `service start`.
1426
+ if (state.isExternalOpenCode && !sharedServiceAttached) return;
1427
+ if (!sharedServiceAttached && !state.openCodeProcess) return;
1428
+ if (state.isShuttingDown || state.isRestartingOpenCode) return;
996
1429
  if (healthCheckCyclePromise) return healthCheckCyclePromise;
997
1430
 
998
1431
  healthCheckCyclePromise = (async () => {
999
1432
  const healthy = await probeOpenCodeHealth();
1000
1433
  if (!healthy) {
1001
- if (!isManagedOpenCodeProcessAlive()) {
1434
+ const processAlive = sharedServiceAttached
1435
+ ? isProcessAlive(sharedServicePid)
1436
+ : isManagedOpenCodeProcessAlive();
1437
+ if (!processAlive) {
1002
1438
  console.log(`[lifecycle] ${source} health check: OpenCode process exited, restarting...`);
1003
1439
  consecutiveHealthFailures = 0;
1004
1440
  lastHealthProbeResult = null;
@@ -1017,6 +1453,11 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
1017
1453
  await restartOpenCode();
1018
1454
  } else {
1019
1455
  resetHealthFailureState();
1456
+ try {
1457
+ const gate = await probeV1MigrationGate(AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS));
1458
+ applyV1MigrationGate(gate);
1459
+ } catch {
1460
+ }
1020
1461
  }
1021
1462
  })().finally(() => {
1022
1463
  healthCheckCyclePromise = null;
@@ -1051,6 +1492,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
1051
1492
 
1052
1493
  return {
1053
1494
  killProcessOnPort,
1495
+ isSharedOpenCodeService: () => sharedServiceAttached,
1054
1496
  startOpenCode,
1055
1497
  restartOpenCode,
1056
1498
  waitForOpenCodeReady,
@@ -1061,5 +1503,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
1061
1503
  startHealthMonitoring,
1062
1504
  triggerHealthCheck,
1063
1505
  waitForPortRelease,
1506
+ revokeRuntimeExecutionPermit,
1507
+ refreshRuntimeContractFromProbe,
1064
1508
  };
1065
1509
  };