@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
@@ -9,12 +9,14 @@ vi.mock('node:child_process', () => ({
9
9
  }));
10
10
 
11
11
  const { createOpenCodeLifecycleRuntime } = await import('./lifecycle.js');
12
+ const { createOpenCodeNetworkRuntime } = await import('./network-runtime.js');
12
13
 
13
14
  const originalOpencodeBinary = process.env.OPENCODE_BINARY;
14
15
  const originalPath = process.env.PATH;
15
16
 
16
17
  afterEach(() => {
17
18
  spawnMock.mockReset();
19
+ vi.useRealTimers();
18
20
  vi.unstubAllGlobals();
19
21
  if (typeof originalOpencodeBinary === 'string') {
20
22
  process.env.OPENCODE_BINARY = originalOpencodeBinary;
@@ -57,6 +59,7 @@ const createRuntime = (overrides = {}, stateRef = null) => {
57
59
  openCodeApiDetectionTimer: null,
58
60
  lastOpenCodeError: null,
59
61
  isOpenCodeReady: false,
62
+ v1Migration: null,
60
63
  openCodeNotReadySince: 0,
61
64
  isExternalOpenCode: false,
62
65
  isShuttingDown: false,
@@ -109,6 +112,53 @@ const createRuntime = (overrides = {}, stateRef = null) => {
109
112
  });
110
113
  };
111
114
 
115
+ const jsonResponse = (body, status = 200) => new Response(JSON.stringify(body), {
116
+ status,
117
+ headers: { 'content-type': 'application/json' },
118
+ });
119
+
120
+ const DEFAULT_V2_HEALTH = { version: '2.0.12', pid: 1, urls: [], paths: { tmp: '/tmp' } };
121
+
122
+ const stubOpenCodeFetch = (overrides = {}) => {
123
+ const fetchMock = vi.fn(async (url, init) => {
124
+ const href = String(url);
125
+ if (href.includes('/api/experimental/migration/v1')) {
126
+ const migration = overrides.migration;
127
+ if (typeof migration === 'function') {
128
+ return migration(url, init);
129
+ }
130
+ if (migration && typeof migration === 'object' && Number.isInteger(migration.httpStatus)) {
131
+ return jsonResponse(migration.body ?? {}, migration.httpStatus);
132
+ }
133
+ return jsonResponse(migration ?? { status: 'completed' });
134
+ }
135
+ if (href.includes('/api/info') || href.includes('/global/health')) {
136
+ if (overrides.reuseDefaultServe !== true && href.includes(':4096/')) {
137
+ return new Response('not found', { status: 404 });
138
+ }
139
+ const health = overrides.health;
140
+ if (typeof health === 'function') {
141
+ return health(url, init);
142
+ }
143
+ return jsonResponse(health ?? DEFAULT_V2_HEALTH);
144
+ }
145
+ return new Response('not found', { status: 404 });
146
+ });
147
+ vi.stubGlobal('fetch', fetchMock);
148
+ return fetchMock;
149
+ };
150
+
151
+ const startListeningChild = () => {
152
+ const child = createMockChild();
153
+ spawnMock.mockImplementationOnce(() => {
154
+ queueMicrotask(() => {
155
+ child.stdout.emit('data', 'server listening on http://127.0.0.1:45678\n');
156
+ });
157
+ return child;
158
+ });
159
+ return child;
160
+ };
161
+
112
162
  describe('OpenCode lifecycle', () => {
113
163
  it('launches managed OpenCode with the managed PATH', async () => {
114
164
  delete process.env.OPENCODE_BINARY;
@@ -133,6 +183,173 @@ describe('OpenCode lifecycle', () => {
133
183
  await server.close();
134
184
  });
135
185
 
186
+ it('allows a polite SIGTERM close that finishes after 400ms without escalating early', async () => {
187
+ vi.useFakeTimers();
188
+ delete process.env.OPENCODE_BINARY;
189
+ const child = createMockChild();
190
+ child.kill = vi.fn((signal) => {
191
+ if (signal === 'SIGTERM') {
192
+ // Slow-but-healthy flush: exits after 800ms (>400ms), still under 2500ms grace.
193
+ setTimeout(() => {
194
+ child.signalCode = 'SIGTERM';
195
+ child.emit('close', null, 'SIGTERM');
196
+ }, 800);
197
+ }
198
+ return true;
199
+ });
200
+ spawnMock.mockImplementationOnce(() => {
201
+ queueMicrotask(() => {
202
+ child.stdout.emit('data', 'server listening on http://127.0.0.1:45678\n');
203
+ });
204
+ return child;
205
+ });
206
+ stubOpenCodeFetch();
207
+
208
+ const runtime = createRuntime();
209
+ const server = await runtime.startOpenCode();
210
+ const closePromise = server.close();
211
+
212
+ await vi.advanceTimersByTimeAsync(400);
213
+ expect(child.kill).toHaveBeenCalledWith('SIGTERM');
214
+ expect(child.kill).not.toHaveBeenCalledWith('SIGKILL');
215
+
216
+ await vi.advanceTimersByTimeAsync(400);
217
+ await closePromise;
218
+ expect(child.kill).not.toHaveBeenCalledWith('SIGKILL');
219
+ vi.useRealTimers();
220
+ });
221
+
222
+ it('escalates a hung managed child to SIGKILL only after the full 2500ms SIGTERM grace', async () => {
223
+ vi.useFakeTimers();
224
+ delete process.env.OPENCODE_BINARY;
225
+ const child = createMockChild();
226
+ child.kill = vi.fn((signal) => {
227
+ if (signal === 'SIGKILL') {
228
+ child.signalCode = 'SIGKILL';
229
+ queueMicrotask(() => child.emit('close', null, 'SIGKILL'));
230
+ }
231
+ return true;
232
+ });
233
+ spawnMock.mockImplementationOnce(() => {
234
+ queueMicrotask(() => {
235
+ child.stdout.emit('data', 'server listening on http://127.0.0.1:45678\n');
236
+ });
237
+ return child;
238
+ });
239
+ stubOpenCodeFetch();
240
+
241
+ const runtime = createRuntime();
242
+ const server = await runtime.startOpenCode();
243
+ const closePromise = server.close();
244
+
245
+ await vi.advanceTimersByTimeAsync(2499);
246
+ expect(child.kill).toHaveBeenCalledWith('SIGTERM');
247
+ expect(child.kill).not.toHaveBeenCalledWith('SIGKILL');
248
+
249
+ await vi.advanceTimersByTimeAsync(1);
250
+ expect(child.kill).toHaveBeenCalledWith('SIGKILL');
251
+ await vi.advanceTimersByTimeAsync(1000);
252
+ await closePromise;
253
+ vi.useRealTimers();
254
+ });
255
+
256
+ it('killProcessOnPort only signals the owned pid and never mass-kills by port', async () => {
257
+ const killSpy = vi.spyOn(process, 'kill').mockImplementation(() => true);
258
+ delete process.env.OPENCODE_BINARY;
259
+ const child = createMockChild();
260
+ spawnMock.mockImplementationOnce(() => {
261
+ queueMicrotask(() => {
262
+ child.stdout.emit('data', 'server listening on http://127.0.0.1:45678\n');
263
+ });
264
+ return child;
265
+ });
266
+ stubOpenCodeFetch();
267
+
268
+ const runtime = createRuntime();
269
+ await runtime.startOpenCode();
270
+ killSpy.mockClear();
271
+
272
+ runtime.killProcessOnPort(45678);
273
+ expect(killSpy).not.toHaveBeenCalled();
274
+
275
+ runtime.killProcessOnPort(45678, 4242);
276
+ expect(killSpy).toHaveBeenCalledWith(-4242, 'SIGKILL');
277
+ expect(killSpy).toHaveBeenCalledWith(4242, 'SIGKILL');
278
+ expect(killSpy.mock.calls.every(([target]) => target === -4242 || target === 4242)).toBe(true);
279
+
280
+ killSpy.mockRestore();
281
+ });
282
+
283
+ it('parses a v2 server listening line without the opencode prefix', async () => {
284
+ delete process.env.OPENCODE_BINARY;
285
+ const child = createMockChild();
286
+ spawnMock.mockImplementationOnce(() => {
287
+ queueMicrotask(() => {
288
+ child.stdout.emit('data', 'server listening on http://127.0.0.1:45678\n');
289
+ });
290
+ return child;
291
+ });
292
+
293
+ const runtime = createRuntime();
294
+ const server = await runtime.startOpenCode();
295
+
296
+ expect(server.url).toBe('http://127.0.0.1:45678');
297
+
298
+ await server.close();
299
+ });
300
+
301
+ it('probes /api/info with Basic auth after the managed server is listening', async () => {
302
+ delete process.env.OPENCODE_BINARY;
303
+ const child = createMockChild();
304
+ spawnMock.mockImplementationOnce(() => {
305
+ queueMicrotask(() => {
306
+ child.stdout.emit('data', 'server listening on http://127.0.0.1:45678\n');
307
+ });
308
+ return child;
309
+ });
310
+
311
+ const fetchMock = vi.fn(async (url) => {
312
+ if (String(url).includes('/api/info')) {
313
+ return new Response(JSON.stringify(DEFAULT_V2_HEALTH), {
314
+ status: 200,
315
+ headers: { 'content-type': 'application/json' },
316
+ });
317
+ }
318
+ if (String(url).includes('/api/experimental/migration/v1')) {
319
+ return new Response(JSON.stringify({ status: 'completed' }), {
320
+ status: 200,
321
+ headers: { 'content-type': 'application/json' },
322
+ });
323
+ }
324
+ return new Response('not found', { status: 404 });
325
+ });
326
+ vi.stubGlobal('fetch', fetchMock);
327
+
328
+ const authHeaders = { Authorization: 'Basic Zml4dHVyZQ==' };
329
+ const network = createOpenCodeNetworkRuntime({
330
+ state: {
331
+ openCodePort: 45678,
332
+ openCodeBaseUrl: null,
333
+ openCodeApiPrefix: '',
334
+ openCodeApiPrefixDetected: false,
335
+ openCodeApiDetectionTimer: null,
336
+ },
337
+ getOpenCodeAuthHeaders: () => authHeaders,
338
+ });
339
+ const runtime = createRuntime({
340
+ getOpenCodeAuthHeaders: () => authHeaders,
341
+ waitForReady: network.waitForReady,
342
+ });
343
+ const server = await runtime.startOpenCode();
344
+ const healthCall = fetchMock.mock.calls.find((call) => String(call[0]).includes('/api/info'));
345
+
346
+ expect(healthCall).toBeDefined();
347
+ expect(String(healthCall[0])).toContain('/api/info');
348
+ expect(healthCall[1].headers.Authorization).toMatch(/^Basic /);
349
+
350
+ await server.close();
351
+ });
352
+
136
353
  it('prepares a fresh managed capability environment and records the spawned child pid', async () => {
137
354
  const child = createMockChild();
138
355
  spawnMock.mockImplementationOnce(() => {
@@ -298,7 +515,7 @@ describe('OpenCode lifecycle', () => {
298
515
  stateRef.current.openCodeProcess = managedChild;
299
516
  stateRef.current.openCodePort = 45678;
300
517
  stateRef.current.isExternalOpenCode = false;
301
- vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ healthy: true }), { status: 200 })));
518
+ stubOpenCodeFetch();
302
519
  await runtime.bootstrapOpenCodeAtStartup();
303
520
  expect(managedChild.close).toHaveBeenCalledTimes(1);
304
521
  expect(stateRef.current.openCodeProcess).toBeNull();
@@ -324,7 +541,7 @@ describe('OpenCode lifecycle', () => {
324
541
  }, stateRef);
325
542
  stateRef.current.isExternalOpenCode = true;
326
543
  stateRef.current.openCodeBaseUrl = 'http://127.0.0.1:3001';
327
- vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ healthy: true }), { status: 200 })));
544
+ stubOpenCodeFetch();
328
545
 
329
546
  await runtime.bootstrapOpenCodeAtStartup();
330
547
 
@@ -367,6 +584,7 @@ describe('OpenCode lifecycle', () => {
367
584
  clearResolvedOpenCodeBinary,
368
585
  }, stateRef);
369
586
  const errorLog = vi.spyOn(console, 'error').mockImplementation(() => {});
587
+ stubOpenCodeFetch();
370
588
 
371
589
  await runtime.bootstrapOpenCodeAtStartup();
372
590
  expect(spawnMock).not.toHaveBeenCalled();
@@ -378,10 +596,7 @@ describe('OpenCode lifecycle', () => {
378
596
  });
379
597
  return child;
380
598
  });
381
- vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ healthy: true }), {
382
- status: 200,
383
- headers: { 'content-type': 'application/json' },
384
- })));
599
+ stubOpenCodeFetch();
385
600
 
386
601
  try {
387
602
  await runtime.retryOpenCodeStartup();
@@ -393,4 +608,481 @@ describe('OpenCode lifecycle', () => {
393
608
  await stateRef.current.openCodeProcess?.close();
394
609
  }
395
610
  });
611
+
612
+ it('does not mark OpenCode ready for messages while V1 migration is required', async () => {
613
+ delete process.env.OPENCODE_BINARY;
614
+ startListeningChild();
615
+ const stateRef = {};
616
+ stubOpenCodeFetch({ migration: { status: 'required' } });
617
+ const runtime = createRuntime({}, stateRef);
618
+
619
+ const server = await runtime.startOpenCode();
620
+
621
+ expect(stateRef.current.isOpenCodeReady).toBe(false);
622
+ expect(stateRef.current.v1Migration).toMatchObject({
623
+ admitTranscript: false,
624
+ phase: 'required',
625
+ });
626
+
627
+ await server.close();
628
+ });
629
+
630
+ it('keeps the ready gate closed while waitForOpenCodeReady sees required migration', async () => {
631
+ const stateRef = {};
632
+ const runtime = createRuntime({}, stateRef);
633
+ stateRef.current.openCodePort = 45678;
634
+ stubOpenCodeFetch({
635
+ migration: { status: 'required' },
636
+ });
637
+
638
+ await expect(runtime.waitForOpenCodeReady(250, 40)).rejects.toThrow(/V1 migration is required/);
639
+ expect(stateRef.current.isOpenCodeReady).toBe(false);
640
+ expect(stateRef.current.v1Migration).toMatchObject({
641
+ admitTranscript: false,
642
+ phase: 'required',
643
+ });
644
+ });
645
+
646
+ it('marks OpenCode ready for messages after V1 migration completed', async () => {
647
+ delete process.env.OPENCODE_BINARY;
648
+ startListeningChild();
649
+ const stateRef = {};
650
+ stubOpenCodeFetch({ migration: { status: 'completed' } });
651
+ const runtime = createRuntime({}, stateRef);
652
+
653
+ const server = await runtime.startOpenCode();
654
+
655
+ expect(stateRef.current.isOpenCodeReady).toBe(true);
656
+ expect(stateRef.current.v1Migration).toMatchObject({
657
+ admitTranscript: true,
658
+ phase: 'completed',
659
+ });
660
+
661
+ await server.close();
662
+ });
663
+
664
+ it('admits messages when V1 migration is absent', async () => {
665
+ const stateRef = {};
666
+ const runtime = createRuntime({}, stateRef);
667
+ stateRef.current.openCodePort = 45678;
668
+ stubOpenCodeFetch({
669
+ migration: { httpStatus: 404, body: {} },
670
+ });
671
+
672
+ await runtime.waitForOpenCodeReady(1000, 40);
673
+
674
+ expect(stateRef.current.isOpenCodeReady).toBe(true);
675
+ expect(stateRef.current.v1Migration).toMatchObject({
676
+ admitTranscript: true,
677
+ phase: 'absent',
678
+ });
679
+ });
680
+
681
+ it('retries V1 migration error and still blocks messages until completed', async () => {
682
+ const stateRef = {};
683
+ const runtime = createRuntime({}, stateRef);
684
+ stateRef.current.openCodePort = 45678;
685
+ let migrationCalls = 0;
686
+ const fetchMock = stubOpenCodeFetch({
687
+ migration: () => {
688
+ migrationCalls += 1;
689
+ if (migrationCalls === 1) {
690
+ return jsonResponse({ status: 'error', error: 'locked' });
691
+ }
692
+ return jsonResponse({ status: 'completed' });
693
+ },
694
+ });
695
+
696
+ await runtime.waitForOpenCodeReady(2000, 40);
697
+
698
+ expect(migrationCalls).toBeGreaterThan(1);
699
+ expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/experimental/migration/v1'))).toBe(true);
700
+ expect(fetchMock.mock.calls.every((call) => !call[1] || call[1].method !== 'POST')).toBe(true);
701
+ expect(stateRef.current.isOpenCodeReady).toBe(true);
702
+ expect(stateRef.current.v1Migration).toMatchObject({
703
+ admitTranscript: true,
704
+ phase: 'completed',
705
+ });
706
+ });
707
+
708
+ it('publishes v1Migration on lifecycle state for health snapshot', async () => {
709
+ const stateRef = {};
710
+ const runtime = createRuntime({}, stateRef);
711
+ stateRef.current.openCodePort = 45678;
712
+ stubOpenCodeFetch({
713
+ migration: {
714
+ status: 'running',
715
+ progress: { label: 'Sessions', numerator: 2, denominator: 8 },
716
+ },
717
+ });
718
+
719
+ await expect(runtime.waitForOpenCodeReady(200, 40)).rejects.toThrow(/V1 migration is running/);
720
+ expect(stateRef.current.isOpenCodeReady).toBe(false);
721
+ expect(stateRef.current.v1Migration).toMatchObject({
722
+ admitTranscript: false,
723
+ phase: 'running',
724
+ progress: { label: 'Sessions', numerator: 2, denominator: 8 },
725
+ });
726
+ expect(stateRef.current.v1Migration.userNotice).toContain('V1 subtasks do not appear in v2');
727
+ });
728
+
729
+ it('clears v1Migration when an external OpenCode restart probe fails', async () => {
730
+ const stateRef = {};
731
+ const runtime = createRuntime({
732
+ env: {
733
+ ENV_CONFIGURED_OPENCODE_PORT: null,
734
+ ENV_CONFIGURED_OPENCODE_HOST: null,
735
+ ENV_EFFECTIVE_PORT: 45678,
736
+ ENV_CONFIGURED_OPENCODE_HOSTNAME: '127.0.0.1',
737
+ ENV_SKIP_OPENCODE_START: false,
738
+ },
739
+ }, stateRef);
740
+ stateRef.current.isExternalOpenCode = true;
741
+ stateRef.current.openCodePort = 45678;
742
+ stateRef.current.openCodeBaseUrl = 'http://127.0.0.1:45678';
743
+ stateRef.current.v1Migration = { admitTranscript: true, phase: 'completed' };
744
+ vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ healthy: false }), {
745
+ status: 200,
746
+ headers: { 'content-type': 'application/json' },
747
+ })));
748
+
749
+ await expect(runtime.restartOpenCode()).rejects.toThrow('External OpenCode server on port 45678 is not responding');
750
+ expect(stateRef.current.isOpenCodeReady).toBe(false);
751
+ expect(stateRef.current.v1Migration).toBeNull();
752
+ // Read-only external mount: failed re-probe keeps port + ownership; never spawn.
753
+ expect(stateRef.current.openCodePort).toBe(45678);
754
+ expect(stateRef.current.isExternalOpenCode).toBe(true);
755
+ expect(spawnMock).not.toHaveBeenCalled();
756
+ });
757
+
758
+ it('rejects healthy 1.x health bodies instead of admitting the process', async () => {
759
+ const stateRef = {};
760
+ const runtime = createRuntime({}, stateRef);
761
+ stateRef.current.openCodePort = 45678;
762
+ stubOpenCodeFetch({ health: { healthy: true, version: '1.15.0' } });
763
+
764
+ await expect(runtime.waitForOpenCodeReady(300, 40)).rejects.toThrow(/unhealthy|Timed out|1\.|version/i);
765
+ expect(stateRef.current.isOpenCodeReady).toBe(false);
766
+ });
767
+
768
+ it('rejects healthy responses with a missing version', async () => {
769
+ const stateRef = {};
770
+ const runtime = createRuntime({}, stateRef);
771
+ stateRef.current.openCodePort = 45678;
772
+ stubOpenCodeFetch({ health: { healthy: true } });
773
+
774
+ await expect(runtime.waitForOpenCodeReady(300, 40)).rejects.toThrow();
775
+ expect(stateRef.current.isOpenCodeReady).toBe(false);
776
+ });
777
+
778
+ it('health version + migration matrix: v2+completed admits, v2+running blocks, 1.x blocks', async () => {
779
+ const cases = [
780
+ { health: DEFAULT_V2_HEALTH, migration: { status: 'completed' }, admit: true },
781
+ { health: DEFAULT_V2_HEALTH, migration: { status: 'running' }, admit: false },
782
+ { health: DEFAULT_V2_HEALTH, migration: { status: 'required' }, admit: false },
783
+ { health: DEFAULT_V2_HEALTH, migration: { status: 'error', error: 'disk' }, admit: false },
784
+ { health: { healthy: true, version: '1.18.18' }, migration: { status: 'completed' }, admit: false },
785
+ ];
786
+
787
+ for (const entry of cases) {
788
+ const stateRef = {};
789
+ const runtime = createRuntime({}, stateRef);
790
+ stateRef.current.openCodePort = 45678;
791
+ stubOpenCodeFetch({ health: entry.health, migration: entry.migration });
792
+ if (entry.admit) {
793
+ await runtime.waitForOpenCodeReady(1000, 40);
794
+ expect(stateRef.current.isOpenCodeReady).toBe(true);
795
+ } else {
796
+ await expect(runtime.waitForOpenCodeReady(250, 40)).rejects.toThrow();
797
+ expect(stateRef.current.isOpenCodeReady).toBe(false);
798
+ }
799
+ }
800
+ });
801
+
802
+ it('aborts a hung migration probe with the same per-attempt signal after health', async () => {
803
+ const stateRef = {};
804
+ const runtime = createRuntime({}, stateRef);
805
+ stateRef.current.openCodePort = 45678;
806
+ let migrationStarted = 0;
807
+ let sawAbort = false;
808
+ stubOpenCodeFetch({
809
+ health: DEFAULT_V2_HEALTH,
810
+ migration: (_url, init) => {
811
+ migrationStarted += 1;
812
+ return new Promise((_resolve, reject) => {
813
+ const signal = init?.signal;
814
+ if (!signal) {
815
+ reject(new Error('migration probe missing abort signal'));
816
+ return;
817
+ }
818
+ if (signal.aborted) {
819
+ sawAbort = true;
820
+ reject(new DOMException('Aborted', 'AbortError'));
821
+ return;
822
+ }
823
+ signal.addEventListener('abort', () => {
824
+ sawAbort = true;
825
+ reject(new DOMException('Aborted', 'AbortError'));
826
+ }, { once: true });
827
+ });
828
+ },
829
+ });
830
+
831
+ // Default per-attempt health timeout is 5s; outer deadline must exceed it once
832
+ // so the hung migration is aborted by the attempt timer, not only the loop exit.
833
+ const started = Date.now();
834
+ await expect(runtime.waitForOpenCodeReady(6500, 50)).rejects.toThrow();
835
+ const elapsed = Date.now() - started;
836
+ expect(migrationStarted).toBeGreaterThan(0);
837
+ expect(sawAbort).toBe(true);
838
+ expect(elapsed).toBeLessThan(12_000);
839
+ expect(stateRef.current.isOpenCodeReady).toBe(false);
840
+ }, 15_000);
841
+
842
+ it('revokes execution permit on restart and ignores stale async health for a new generation', async () => {
843
+ const stateRef = {};
844
+ const runtime = createRuntime({}, stateRef);
845
+ stateRef.current.openCodePort = 45678;
846
+ stateRef.current.runtimeContractGeneration = 1;
847
+ stateRef.current.runtimeContract = {
848
+ executionAllowed: true,
849
+ phase: 'ready',
850
+ serveVersion: '2.0.12',
851
+ instanceGeneration: 1,
852
+ };
853
+
854
+ const genBefore = runtime.revokeRuntimeExecutionPermit('test-restart');
855
+ expect(genBefore).toBe(2);
856
+ expect(stateRef.current.runtimeContract.executionAllowed).toBe(false);
857
+ expect(stateRef.current.runtimeContract.phase).toBe('pending');
858
+ expect(stateRef.current.runtimeContract.instanceGeneration).toBe(2);
859
+
860
+ // Stale probe from generation 1 must not re-open execution on generation 2.
861
+ runtime.refreshRuntimeContractFromProbe(
862
+ {
863
+ ok: true,
864
+ version: '2.0.12',
865
+ authenticated: true,
866
+ healthOk: true,
867
+ },
868
+ { admitTranscript: true, phase: 'done', error: null },
869
+ 1,
870
+ );
871
+ expect(stateRef.current.runtimeContract.executionAllowed).toBe(false);
872
+ expect(stateRef.current.runtimeContract.instanceGeneration).toBe(2);
873
+
874
+ // Matching generation publishes verified permit.
875
+ runtime.refreshRuntimeContractFromProbe(
876
+ {
877
+ ok: true,
878
+ version: '2.0.12',
879
+ authenticated: true,
880
+ healthOk: true,
881
+ },
882
+ { admitTranscript: true, phase: 'done', error: null },
883
+ 2,
884
+ );
885
+ expect(stateRef.current.runtimeContract.executionAllowed).toBe(true);
886
+ expect(stateRef.current.runtimeContract.instanceGeneration).toBe(2);
887
+ expect(stateRef.current.runtimeContract.serveVersion).toBe('2.0.12');
888
+ });
889
+
890
+ it('does not mark external skip-start ready before version and migration admit', async () => {
891
+ const stateRef = {};
892
+ const runtime = createRuntime({
893
+ env: {
894
+ ENV_CONFIGURED_OPENCODE_PORT: null,
895
+ ENV_CONFIGURED_OPENCODE_HOST: { origin: 'http://127.0.0.1:3001' },
896
+ ENV_EFFECTIVE_PORT: 3001,
897
+ ENV_CONFIGURED_OPENCODE_HOSTNAME: '127.0.0.1',
898
+ ENV_SKIP_OPENCODE_START: true,
899
+ },
900
+ syncFromHmrState: vi.fn(),
901
+ }, stateRef);
902
+ // Reject health so bootstrap's waitForOpenCodeReady fails quickly without a
903
+ // 20s migration spin, while still proving skip-start never pre-admits ready.
904
+ stubOpenCodeFetch({ health: { healthy: true, version: '1.15.0' } });
905
+
906
+ await runtime.bootstrapOpenCodeAtStartup();
907
+
908
+ expect(stateRef.current.isExternalOpenCode).toBe(true);
909
+ expect(stateRef.current.isOpenCodeReady).toBe(false);
910
+ expect(stateRef.current.openCodePort).toBe(3001);
911
+ }, 25_000);
912
+ });
913
+
914
+ describe('shared official OpenCode service', () => {
915
+ const UNCONFIGURED_ENV = {
916
+ ENV_CONFIGURED_OPENCODE_PORT: null,
917
+ ENV_CONFIGURED_OPENCODE_HOST: null,
918
+ ENV_EFFECTIVE_PORT: null,
919
+ ENV_CONFIGURED_OPENCODE_HOSTNAME: '127.0.0.1',
920
+ ENV_SKIP_OPENCODE_START: false,
921
+ };
922
+ const REGISTRATION = {
923
+ origin: 'http://127.0.0.1:49374',
924
+ port: 49374,
925
+ pid: process.pid,
926
+ password: 'service-password',
927
+ version: '2.0.12',
928
+ };
929
+
930
+ const createSharedService = (overrides = {}) => ({
931
+ registrationFile: '/state/opencode/service.json',
932
+ buildStartEnv: (env) => {
933
+ const { OPENCODE_SERVER_PASSWORD: _password, ...rest } = env;
934
+ return rest;
935
+ },
936
+ start: vi.fn(async () => {}),
937
+ readRegistration: vi.fn(async () => REGISTRATION),
938
+ ...overrides,
939
+ });
940
+
941
+ const stubSharedFetch = () => {
942
+ const base = stubOpenCodeFetch();
943
+ const fetchMock = vi.fn(async (url, init) => (String(url).includes('/api/location/reload')
944
+ ? new Response(null, { status: 204 })
945
+ : base(url, init)));
946
+ vi.stubGlobal('fetch', fetchMock);
947
+ return fetchMock;
948
+ };
949
+
950
+ it('attaches to the registered service instead of spawning a private serve', async () => {
951
+ process.env.OPENCODE_BINARY = '/mock/opencode';
952
+ const sharedService = createSharedService();
953
+ const adoptOpenCodeServerPassword = vi.fn();
954
+ const stateRef = {};
955
+ const runtime = createRuntime({ env: UNCONFIGURED_ENV, sharedService, adoptOpenCodeServerPassword }, stateRef);
956
+ const fetchMock = stubSharedFetch();
957
+
958
+ await runtime.bootstrapOpenCodeAtStartup();
959
+
960
+ expect(spawnMock).not.toHaveBeenCalled();
961
+ expect(sharedService.start).toHaveBeenCalledTimes(1);
962
+ const startInput = sharedService.start.mock.calls[0][0];
963
+ expect(startInput.binary).toBe('/mock/opencode');
964
+ expect(startInput.env.OPENCODE_SERVER_PASSWORD).toBeUndefined();
965
+ expect(adoptOpenCodeServerPassword).toHaveBeenCalledWith('service-password', 'shared-service');
966
+ expect(stateRef.current.isExternalOpenCode).toBe(true);
967
+ expect(stateRef.current.openCodeBaseUrl).toBe('http://127.0.0.1:49374');
968
+ expect(stateRef.current.openCodePort).toBe(49374);
969
+ expect(runtime.isSharedOpenCodeService()).toBe(true);
970
+ // The leftover private serve on 4096 is never probed once the service wins.
971
+ expect(fetchMock.mock.calls.some(([url]) => String(url).includes(':4096/'))).toBe(false);
972
+ });
973
+
974
+ it('reports shared startup failure without probing or spawning a private serve, and can retry', async () => {
975
+ process.env.OPENCODE_BINARY = '/mock/opencode';
976
+ const sharedService = createSharedService({ start: vi.fn(async () => { throw new Error('boom'); }) });
977
+ const adoptOpenCodeServerPassword = vi.fn();
978
+ const stateRef = {};
979
+ const runtime = createRuntime({ env: UNCONFIGURED_ENV, sharedService, adoptOpenCodeServerPassword }, stateRef);
980
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
981
+ const fetchMock = stubSharedFetch();
982
+
983
+ await runtime.bootstrapOpenCodeAtStartup();
984
+
985
+ expect(adoptOpenCodeServerPassword).toHaveBeenLastCalledWith(null, null);
986
+ expect(runtime.isSharedOpenCodeService()).toBe(false);
987
+ expect(stateRef.current.isExternalOpenCode).toBe(false);
988
+ expect(stateRef.current.openCodeBaseUrl).toBeNull();
989
+ expect(stateRef.current.openCodeProcess).toBeNull();
990
+ expect(stateRef.current.isOpenCodeReady).toBe(false);
991
+ expect(stateRef.current.lastOpenCodeError).toBe('boom');
992
+ expect(spawnMock).not.toHaveBeenCalled();
993
+ expect(fetchMock).not.toHaveBeenCalled();
994
+
995
+ sharedService.start.mockResolvedValue(undefined);
996
+ await runtime.bootstrapOpenCodeAtStartup();
997
+ expect(runtime.isSharedOpenCodeService()).toBe(true);
998
+ expect(stateRef.current.isOpenCodeReady).toBe(true);
999
+ });
1000
+
1001
+ it('reconnects using updated registration without replacing the service', async () => {
1002
+ const sharedService = createSharedService();
1003
+ const adoptOpenCodeServerPassword = vi.fn();
1004
+ const stateRef = {};
1005
+ const runtime = createRuntime({ env: UNCONFIGURED_ENV, sharedService, adoptOpenCodeServerPassword }, stateRef);
1006
+ stubSharedFetch();
1007
+ await runtime.bootstrapOpenCodeAtStartup();
1008
+ sharedService.readRegistration.mockResolvedValue({ ...REGISTRATION, port: 49375, origin: 'http://127.0.0.1:49375', password: 'rotated' });
1009
+
1010
+ await runtime.restartOpenCode();
1011
+
1012
+ expect(sharedService.start.mock.lastCall[0].replace).toBe(false);
1013
+ expect(stateRef.current.openCodePort).toBe(49375);
1014
+ expect(adoptOpenCodeServerPassword).toHaveBeenLastCalledWith('rotated', 'shared-service');
1015
+ expect(spawnMock).not.toHaveBeenCalled();
1016
+ });
1017
+
1018
+ it('reattaches shared ownership across HMR and retires an old private child', async () => {
1019
+ const sharedService = createSharedService();
1020
+ const stateRef = {};
1021
+ const runtime = createRuntime({ env: UNCONFIGURED_ENV, sharedService }, stateRef);
1022
+ const close = vi.fn();
1023
+ stateRef.current.openCodeProcess = { pid: 12345, close };
1024
+ stateRef.current.openCodePort = 4096;
1025
+ stubSharedFetch();
1026
+ await runtime.bootstrapOpenCodeAtStartup();
1027
+ expect(close).toHaveBeenCalledOnce();
1028
+ expect(stateRef.current.openCodeProcess).toBeNull();
1029
+ expect(runtime.isSharedOpenCodeService()).toBe(true);
1030
+ await runtime.bootstrapOpenCodeAtStartup();
1031
+ expect(runtime.isSharedOpenCodeService()).toBe(true);
1032
+ expect(stateRef.current.isExternalOpenCode).toBe(true);
1033
+ expect(spawnMock).not.toHaveBeenCalled();
1034
+ });
1035
+
1036
+ it('uses the explicit upgrade binary without settings rediscovery and propagates replacement failures', async () => {
1037
+ const sharedService = createSharedService();
1038
+ const applyOpencodeBinaryFromSettings = vi.fn();
1039
+ const stateRef = {};
1040
+ const runtime = createRuntime({ env: UNCONFIGURED_ENV, sharedService, applyOpencodeBinaryFromSettings }, stateRef);
1041
+ stubSharedFetch();
1042
+ await runtime.bootstrapOpenCodeAtStartup();
1043
+ applyOpencodeBinaryFromSettings.mockClear();
1044
+
1045
+ await runtime.restartOpenCode({ binaryPath: '/cache/opencode-cli/2.0.14/opencode' });
1046
+
1047
+ expect(sharedService.start.mock.lastCall[0]).toMatchObject({ binary: '/cache/opencode-cli/2.0.14/opencode', replace: true });
1048
+ expect(applyOpencodeBinaryFromSettings).not.toHaveBeenCalled();
1049
+ sharedService.start.mockRejectedValue(new Error('replacement failed'));
1050
+ await expect(runtime.restartOpenCode({ binaryPath: '/cache/opencode-cli/2.0.14/opencode' })).rejects.toThrow('replacement failed');
1051
+ expect(stateRef.current.isOpenCodeReady).toBe(false);
1052
+ expect(stateRef.current.runtimeContract.executionAllowed).toBe(false);
1053
+ expect(spawnMock).not.toHaveBeenCalled();
1054
+ });
1055
+
1056
+ it('reloads config changes in place instead of restarting the shared service', async () => {
1057
+ process.env.OPENCODE_BINARY = '/mock/opencode';
1058
+ const sharedService = createSharedService();
1059
+ const stateRef = {};
1060
+ const runtime = createRuntime({ env: UNCONFIGURED_ENV, sharedService, adoptOpenCodeServerPassword: vi.fn() }, stateRef);
1061
+ const fetchMock = stubSharedFetch();
1062
+ await runtime.bootstrapOpenCodeAtStartup();
1063
+ fetchMock.mockClear();
1064
+
1065
+ const result = await runtime.refreshOpenCodeAfterConfigChange('plugin add');
1066
+
1067
+ expect(result).toEqual({ reloaded: true, external: true });
1068
+ const reloadCalls = fetchMock.mock.calls.filter(([url]) => String(url).includes('/api/location/reload'));
1069
+ expect(reloadCalls).toHaveLength(1);
1070
+ expect(reloadCalls[0][1]?.method).toBe('POST');
1071
+ expect(spawnMock).not.toHaveBeenCalled();
1072
+ expect(sharedService.start).toHaveBeenCalledTimes(1);
1073
+ });
1074
+
1075
+ it('keeps an explicitly configured OpenCode port instead of the shared service', async () => {
1076
+ const sharedService = createSharedService();
1077
+ const stateRef = {};
1078
+ const runtime = createRuntime({ sharedService, adoptOpenCodeServerPassword: vi.fn() }, stateRef);
1079
+ stubSharedFetch();
1080
+ startListeningChild();
1081
+
1082
+ await runtime.bootstrapOpenCodeAtStartup();
1083
+
1084
+ expect(sharedService.start).not.toHaveBeenCalled();
1085
+ expect(runtime.isSharedOpenCodeService()).toBe(false);
1086
+ await stateRef.current.openCodeProcess?.close();
1087
+ });
396
1088
  });