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