@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
@@ -2,7 +2,10 @@ 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
4
  import { evaluateOpenCodeHealthBody } from './opencode2-pin.js';
5
+ import { readOpenCode2BinaryVersion } from './ensure-cli.js';
6
+ import { createRevokedRuntimeContract, evaluateRuntimeContract } from './runtime-contract.js';
5
7
  import { OPENCODE_V1_MIGRATION_PATH, fetchV1MigrationGate } from './v1-migration-gate.js';
8
+ import { isProcessAlive } from './shared-service.js';
6
9
 
7
10
  const parsePositiveInt = (value, fallback) => {
8
11
  const parsed = Number.parseInt(String(value ?? ''), 10);
@@ -20,6 +23,10 @@ const HEALTH_CHECK_RESULT_CACHE_MS = parsePositiveInt(process.env.OPENCHAMBER_OP
20
23
  const OPENCODE_HEALTH_PATH = '/api/info';
21
24
  const OPENCODE_HEALTH_FALLBACK_PATH = '/global/health';
22
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;
23
30
  // Last-used directory plus recently opened projects — deeper tails are unlikely
24
31
  // to be the user's first click and just add background work.
25
32
  const WARMUP_DIRECTORY_LIMIT = 4;
@@ -51,23 +58,50 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
51
58
  managedCapabilitiesRuntime = null,
52
59
  getActiveSessionCount = () => 0,
53
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,
54
65
  } = deps;
55
66
 
56
- const killProcessOnPort = (port) => {
57
- if (!port || process.platform === 'win32') return;
58
- try {
59
- const result = spawnSync('lsof', ['-ti', `:${port}`], { encoding: 'utf8', timeout: 5000, windowsHide: true });
60
- const output = result.stdout || '';
61
- const myPid = process.pid;
62
- for (const pidStr of output.split(/\s+/)) {
63
- const pid = parseInt(pidStr.trim(), 10);
64
- if (pid && pid !== myPid) {
65
- try {
66
- spawnSync('kill', ['-9', String(pid)], { stdio: 'ignore', timeout: 2000 });
67
- } catch {
68
- }
69
- }
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 {
70
95
  }
96
+ return;
97
+ }
98
+
99
+ try {
100
+ process.kill(-pid, 'SIGKILL');
101
+ } catch {
102
+ }
103
+ try {
104
+ process.kill(pid, 'SIGKILL');
71
105
  } catch {
72
106
  }
73
107
  };
@@ -216,13 +250,13 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
216
250
 
217
251
  signalProcessTree('SIGTERM');
218
252
 
219
- if (await waitForChildProcessClose(child, 2500)) {
253
+ if (await waitForChildProcessClose(child, OPENCODE_CHILD_SIGTERM_GRACE_MS)) {
220
254
  return;
221
255
  }
222
256
 
223
257
  signalProcessTree('SIGKILL');
224
258
 
225
- await waitForChildProcessClose(child, 1000);
259
+ await waitForChildProcessClose(child, OPENCODE_CHILD_SIGKILL_WAIT_MS);
226
260
  };
227
261
 
228
262
  const closeManagedOpenCodeChild = async (child) => {
@@ -447,14 +481,18 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
447
481
  // v2 readiness lives at /api/info (ServerInfo.version); /global/health remains
448
482
  // a probe fallback for older sidecars. Both require Basic auth from
449
483
  // getOpenCodeAuthHeaders(). Version admission rejects 1.x / missing / noise.
450
- const fetchOpenCodeHealthOk = async (urlForPath, signal, options = {}) => {
484
+ // Health ok is reachability only — execution semantics live on runtimeContract.
485
+ const fetchOpenCodeHealthResult = async (urlForPath, signal, options = {}) => {
451
486
  const headerCandidates = [
452
- { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
487
+ { Accept: 'application/json', ...getOpenCodeAuthHeaders(), authenticated: true },
453
488
  ];
454
489
  if (options.allowUnauthenticated) {
455
- headerCandidates.push({ Accept: 'application/json' });
490
+ headerCandidates.push({ Accept: 'application/json', authenticated: false });
456
491
  }
457
- for (const headers of headerCandidates) {
492
+ let sawUnauthorized = false;
493
+ let lastGate = null;
494
+ for (const headerCandidate of headerCandidates) {
495
+ const { authenticated, ...headers } = headerCandidate;
458
496
  for (const healthPath of [OPENCODE_HEALTH_PATH, OPENCODE_HEALTH_FALLBACK_PATH]) {
459
497
  try {
460
498
  const response = await fetch(urlForPath(healthPath), {
@@ -462,27 +500,115 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
462
500
  headers,
463
501
  signal,
464
502
  });
503
+ if (response.status === 401 || response.status === 403) {
504
+ sawUnauthorized = true;
505
+ continue;
506
+ }
465
507
  if (!response.ok) continue;
466
508
  const body = await response.json().catch(() => null);
467
509
  const gate = evaluateOpenCodeHealthBody(body);
468
- if (gate.ok) return true;
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
+ }
469
519
  } catch {
470
520
  }
471
521
  }
472
522
  }
473
- return false;
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;
474
595
  };
475
596
 
476
597
  const isOpenCodeProcessHealthy = async () => {
477
- if (!state.openCodeProcess || !state.openCodePort) {
598
+ if ((!state.openCodeProcess && !sharedServiceAttached) || !state.openCodePort) {
478
599
  return false;
479
600
  }
480
601
 
602
+ const generationAtStart = Number(state.runtimeContractGeneration) || 0;
481
603
  try {
482
- return await fetchOpenCodeHealthOk(
604
+ const result = await fetchOpenCodeHealthResult(
483
605
  (healthPath) => buildOpenCodeUrl(healthPath, ''),
484
606
  AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS)
485
607
  );
608
+ if (result.ok) {
609
+ refreshRuntimeContractFromProbe(result, state.v1Migration, generationAtStart);
610
+ }
611
+ return result.ok;
486
612
  } catch {
487
613
  return false;
488
614
  }
@@ -493,17 +619,21 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
493
619
  return false;
494
620
  }
495
621
 
622
+ const generationAtStart = Number(state.runtimeContractGeneration) || 0;
496
623
  try {
497
624
  const controller = new AbortController();
498
625
  const timeout = setTimeout(() => controller.abort(), 3000);
499
626
  const base = origin ?? `http://127.0.0.1:${port}`;
500
- const healthy = await fetchOpenCodeHealthOk(
627
+ const result = await fetchOpenCodeHealthResult(
501
628
  (healthPath) => `${base}${healthPath}`,
502
629
  controller.signal,
503
630
  { allowUnauthenticated: true },
504
631
  );
505
632
  clearTimeout(timeout);
506
- return healthy;
633
+ if (result.ok) {
634
+ refreshRuntimeContractFromProbe(result, state.v1Migration, generationAtStart);
635
+ }
636
+ return result.ok;
507
637
  } catch {
508
638
  return false;
509
639
  }
@@ -525,11 +655,114 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
525
655
  throw new Error('Timed out waiting for OpenCode port');
526
656
  };
527
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
+
528
759
  const START_OPEN_CODE_MAX_ATTEMPTS = 2;
529
760
 
530
761
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
531
762
 
532
763
  const startOpenCodeOnce = async () => {
764
+ // New managed instance — drop any prior permit before spawn/probes.
765
+ const generationAtStart = revokeRuntimeExecutionPermit('managed-start');
533
766
  const requestedPort = env.ENV_CONFIGURED_OPENCODE_PORT;
534
767
  const defaultPortFree = requestedPort
535
768
  ? false
@@ -589,8 +822,15 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
589
822
  setOpenCodePort(port);
590
823
  setDetectedOpenCodeApiPrefix(prefix);
591
824
 
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
+ );
592
830
  const gate = await probeV1MigrationGate(AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS));
831
+ refreshRuntimeContractFromProbe(healthResult, gate, generationAtStart);
593
832
  if (applyV1MigrationGate(gate)) {
833
+ refreshRuntimeContractFromProbe(healthResult, gate, generationAtStart);
594
834
  state.lastOpenCodeError = null;
595
835
  }
596
836
 
@@ -606,6 +846,14 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
606
846
  const message = error instanceof Error ? error.message : String(error);
607
847
  state.lastOpenCodeError = message;
608
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
+ }
609
857
  syncToHmrState();
610
858
  console.error(`Failed to start OpenCode: ${message}`);
611
859
  throw error;
@@ -639,20 +887,35 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
639
887
  throw lastError;
640
888
  };
641
889
 
642
- const restartOpenCode = async () => {
890
+ const restartOpenCode = async (options = {}) => {
643
891
  if (state.isShuttingDown) return;
644
892
  if (state.currentRestartPromise) {
645
893
  await state.currentRestartPromise;
646
- return;
894
+ if (!options.binaryPath) return;
647
895
  }
648
896
 
649
897
  state.currentRestartPromise = (async () => {
650
898
  state.isRestartingOpenCode = true;
651
899
  state.isOpenCodeReady = false;
652
900
  state.openCodeNotReadySince = Date.now();
901
+ // Drop prior instance permit before any probe / respawn.
902
+ const restartGeneration = revokeRuntimeExecutionPermit('managed-restart');
653
903
  console.log('Restarting OpenCode process...');
654
904
 
655
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
+ }
656
919
  console.log('Re-probing external OpenCode server...');
657
920
  const probePort = state.openCodePort || env.ENV_CONFIGURED_OPENCODE_PORT || 4096;
658
921
  const probeOrigin = state.openCodeBaseUrl ?? env.ENV_CONFIGURED_OPENCODE_HOST?.origin;
@@ -660,8 +923,14 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
660
923
  if (healthy) {
661
924
  console.log(`External OpenCode server on port ${probePort} is healthy`);
662
925
  setOpenCodePort(probePort);
926
+ const healthResult = await fetchOpenCodeHealthResult(
927
+ (healthPath) => buildOpenCodeUrl(healthPath, ''),
928
+ AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS),
929
+ );
663
930
  const gate = await probeV1MigrationGate(AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS));
931
+ refreshRuntimeContractFromProbe(healthResult, gate, restartGeneration);
664
932
  if (applyV1MigrationGate(gate)) {
933
+ refreshRuntimeContractFromProbe(healthResult, gate, restartGeneration);
665
934
  state.lastOpenCodeError = null;
666
935
  }
667
936
  syncToHmrState();
@@ -679,11 +948,13 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
679
948
  return;
680
949
  }
681
950
 
682
- const portToKill = state.openCodePort;
951
+ const portToRelease = state.openCodePort;
683
952
 
684
953
  if (state.openCodeProcess) {
685
954
  console.log('Stopping existing OpenCode process...');
686
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.
687
958
  await state.openCodeProcess.close();
688
959
  } catch (error) {
689
960
  console.warn('Error closing OpenCode process:', error);
@@ -692,9 +963,9 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
692
963
  syncToHmrState();
693
964
  }
694
965
 
695
- killProcessOnPort(portToKill);
696
- if (!(await waitForPortRelease(portToKill, 5000))) {
697
- 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`);
698
969
  }
699
970
 
700
971
  if (env.ENV_CONFIGURED_OPENCODE_PORT) {
@@ -751,28 +1022,42 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
751
1022
 
752
1023
  const deadline = Date.now() + timeoutMs;
753
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;
754
1028
 
755
1029
  while (Date.now() < deadline) {
756
1030
  let timeout = null;
757
1031
  try {
1032
+ if ((Number(state.runtimeContractGeneration) || 0) !== generationAtStart) {
1033
+ throw new Error('OpenCode instance changed while waiting for ready');
1034
+ }
758
1035
  // One timer covers health + migration for this attempt. Clearing after
759
1036
  // health alone let a hung migration outrun HEALTH_CHECK_TIMEOUT_MS and
760
1037
  // the outer deadline when the probe never settled.
761
1038
  const controller = new AbortController();
762
1039
  timeout = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT_MS);
763
- const healthy = await fetchOpenCodeHealthOk(
1040
+ const healthResult = await fetchOpenCodeHealthResult(
764
1041
  (healthPath) => buildOpenCodeUrl(healthPath, ''),
765
1042
  controller.signal
766
1043
  );
767
1044
 
768
- if (!healthy) {
769
- lastError = new Error('OpenCode health endpoint returned unhealthy response');
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
+ );
770
1052
  await new Promise((resolve) => setTimeout(resolve, intervalMs));
771
1053
  continue;
772
1054
  }
773
1055
 
774
1056
  const gate = await probeV1MigrationGate(controller.signal);
1057
+ refreshRuntimeContractFromProbe(healthResult, gate, generationAtStart);
775
1058
  if (applyV1MigrationGate(gate)) {
1059
+ // Re-publish contract after admitTranscript flips ready.
1060
+ refreshRuntimeContractFromProbe(healthResult, gate, generationAtStart);
776
1061
  state.lastOpenCodeError = null;
777
1062
  return;
778
1063
  }
@@ -833,6 +1118,16 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
833
1118
  const { agentName } = options;
834
1119
 
835
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
+
836
1131
  clearResolvedOpenCodeBinary();
837
1132
  await applyOpencodeBinaryFromSettings();
838
1133
 
@@ -901,12 +1196,19 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
901
1196
  state.openCodeBaseUrl = env.ENV_CONFIGURED_OPENCODE_HOST?.origin ?? null;
902
1197
  setOpenCodePort(env.ENV_EFFECTIVE_PORT);
903
1198
  // External attach is not ready until version + V1 migration admit.
1199
+ revokeRuntimeExecutionPermit('external-attach');
904
1200
  state.isOpenCodeReady = false;
905
1201
  state.openCodeNotReadySince = Date.now();
906
1202
  state.isExternalOpenCode = true;
907
1203
  state.lastOpenCodeError = null;
908
1204
  state.v1Migration = null;
909
1205
  syncToHmrState();
1206
+ } else if (
1207
+ sharedService
1208
+ && !env.ENV_CONFIGURED_OPENCODE_PORT
1209
+ && !env.ENV_CONFIGURED_OPENCODE_HOST
1210
+ ) {
1211
+ await attachSharedOpenCodeService();
910
1212
  } else if (await isOpenCodeProcessHealthy() && (managedCapabilitiesRuntime ? managedCapabilitiesRuntime.hasValidIdentity() : true)) {
911
1213
  console.log(`[HMR] Reusing existing OpenCode process on port ${state.openCodePort}`);
912
1214
  } else if (state.openCodeProcess && !state.isExternalOpenCode && await isOpenCodeProcessHealthy()) {
@@ -1119,15 +1421,20 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
1119
1421
 
1120
1422
  const runHealthCheckCycle = async (source) => {
1121
1423
  // External serve is read-only mount: never restart or spawn a second copy
1122
- // because the user's instance looks unhealthy from our probe.
1123
- if (state.isExternalOpenCode) return;
1124
- if (!state.openCodeProcess || state.isShuttingDown || state.isRestartingOpenCode) return;
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;
1125
1429
  if (healthCheckCyclePromise) return healthCheckCyclePromise;
1126
1430
 
1127
1431
  healthCheckCyclePromise = (async () => {
1128
1432
  const healthy = await probeOpenCodeHealth();
1129
1433
  if (!healthy) {
1130
- if (!isManagedOpenCodeProcessAlive()) {
1434
+ const processAlive = sharedServiceAttached
1435
+ ? isProcessAlive(sharedServicePid)
1436
+ : isManagedOpenCodeProcessAlive();
1437
+ if (!processAlive) {
1131
1438
  console.log(`[lifecycle] ${source} health check: OpenCode process exited, restarting...`);
1132
1439
  consecutiveHealthFailures = 0;
1133
1440
  lastHealthProbeResult = null;
@@ -1185,6 +1492,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
1185
1492
 
1186
1493
  return {
1187
1494
  killProcessOnPort,
1495
+ isSharedOpenCodeService: () => sharedServiceAttached,
1188
1496
  startOpenCode,
1189
1497
  restartOpenCode,
1190
1498
  waitForOpenCodeReady,
@@ -1195,5 +1503,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
1195
1503
  startHealthMonitoring,
1196
1504
  triggerHealthCheck,
1197
1505
  waitForPortRelease,
1506
+ revokeRuntimeExecutionPermit,
1507
+ refreshRuntimeContractFromProbe,
1198
1508
  };
1199
1509
  };