@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
|
@@ -11,6 +11,10 @@ import {
|
|
|
11
11
|
extractDirectoryFromRequest,
|
|
12
12
|
isDirectoryTurnAdmissionPath,
|
|
13
13
|
} from './instance-recovery-runtime.js';
|
|
14
|
+
import {
|
|
15
|
+
runtimeContractExecutionBlockedBody,
|
|
16
|
+
shouldBlockRuntimeContractExecution,
|
|
17
|
+
} from './runtime-contract.js';
|
|
14
18
|
import {
|
|
15
19
|
createReasoningOutboundFilter,
|
|
16
20
|
createSseBlockSplitter,
|
|
@@ -20,6 +24,25 @@ import {
|
|
|
20
24
|
readIncludeReasoningQuery,
|
|
21
25
|
stripIncludeReasoningParam,
|
|
22
26
|
} from '../event-stream/reasoning-projection.js';
|
|
27
|
+
import {
|
|
28
|
+
isSessionLifecycleEventType,
|
|
29
|
+
projectSessionLifecyclePayload,
|
|
30
|
+
projectSessionWithStoredMap,
|
|
31
|
+
} from '../session-metadata/session-projection.js';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* v2 protocol mounts every JSON/SSE route under `/api`
|
|
35
|
+
* (`@opencode-ai/protocol` groups). Host traffic is already `/api/*`.
|
|
36
|
+
* Express `app.use('/api', …)` strips that prefix from `req.url`; put it back
|
|
37
|
+
* before talking to opencode2. Passing a root `/session` path to v2 returns
|
|
38
|
+
* the bundled Web UI HTML (`text/html`) and the SDK throws UnsupportedContentType.
|
|
39
|
+
*/
|
|
40
|
+
export const toUpstreamOpenCodeApiPath = (requestUrl) => {
|
|
41
|
+
const raw = typeof requestUrl === 'string' ? requestUrl : '';
|
|
42
|
+
if (!raw || raw === '/') return '/api';
|
|
43
|
+
if (raw.startsWith('/api/') || raw === '/api' || raw.startsWith('/api?')) return raw;
|
|
44
|
+
return raw.startsWith('/') ? `/api${raw}` : `/api/${raw}`;
|
|
45
|
+
};
|
|
23
46
|
|
|
24
47
|
export const createDirectoryQueryCanonicalizer = ({ realpath, ...cacheOptions } = {}) => {
|
|
25
48
|
const realpathCache = createRealpathCache({ fallbackOnError: true, realpath, ...cacheOptions });
|
|
@@ -183,10 +206,19 @@ const sanitizeSessionListItem = (session) => {
|
|
|
183
206
|
};
|
|
184
207
|
|
|
185
208
|
const sanitizeSessionListPayload = (payload) => {
|
|
186
|
-
if (
|
|
187
|
-
return payload;
|
|
209
|
+
if (Array.isArray(payload)) {
|
|
210
|
+
return payload.map((session) => sanitizeSessionListItem(session));
|
|
211
|
+
}
|
|
212
|
+
if (payload && typeof payload === 'object' && Array.isArray(payload.data)) {
|
|
213
|
+
return { ...payload, data: payload.data.map((session) => sanitizeSessionListItem(session)) };
|
|
188
214
|
}
|
|
189
|
-
return payload
|
|
215
|
+
return payload;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const sessionListRecords = (payload) => {
|
|
219
|
+
if (Array.isArray(payload)) return payload;
|
|
220
|
+
if (payload && typeof payload === 'object' && Array.isArray(payload.data)) return payload.data;
|
|
221
|
+
return null;
|
|
190
222
|
};
|
|
191
223
|
|
|
192
224
|
export const isInteractiveSessionRequest = (method, requestUrl) => {
|
|
@@ -221,13 +253,150 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
221
253
|
SSE_UPSTREAM_CONNECT_TIMEOUT_MS,
|
|
222
254
|
SSE_UPSTREAM_STALL_TIMEOUT_MS,
|
|
223
255
|
getRuntime,
|
|
256
|
+
/** Optional DI: `{ allowMissingContract: true }` for unit factories only. */
|
|
257
|
+
getRuntimeContractGateOptions = null,
|
|
224
258
|
getOpenCodeAuthHeaders,
|
|
225
259
|
buildOpenCodeUrl,
|
|
226
260
|
ensureOpenCodeApiPrefix,
|
|
227
261
|
onInteractiveSessionRequest,
|
|
228
262
|
onSessionTurnAdmission,
|
|
263
|
+
// OpenCode 2.x has no session-metadata update route. When the Host store is
|
|
264
|
+
// wired, fold its per-session metadata onto list/detail responses so
|
|
265
|
+
// clients keep reading `session.metadata` where they always did.
|
|
266
|
+
getStoredSessionMetadata = null,
|
|
267
|
+
/** Sync committed snapshot; null = not ready / unknown when store is wired. */
|
|
268
|
+
getStoredSessionMetadataSync = null,
|
|
269
|
+
/** Await Host metadata load before SSE fan-out (resolves ok/false, does not reject). */
|
|
270
|
+
ensureSessionMetadataReady = null,
|
|
271
|
+
/** Best-effort Host metadata cleanup after successful upstream session delete. */
|
|
272
|
+
onSessionDeleted = null,
|
|
229
273
|
} = deps;
|
|
230
274
|
|
|
275
|
+
const storeConfigured = typeof getStoredSessionMetadata === 'function';
|
|
276
|
+
|
|
277
|
+
const waitForHostMetadataReady = async () => {
|
|
278
|
+
if (!storeConfigured) return true;
|
|
279
|
+
if (typeof ensureSessionMetadataReady === 'function') {
|
|
280
|
+
try {
|
|
281
|
+
return await ensureSessionMetadataReady() === true;
|
|
282
|
+
} catch {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Fallback: one getAll attempt.
|
|
287
|
+
try {
|
|
288
|
+
await getStoredSessionMetadata();
|
|
289
|
+
return true;
|
|
290
|
+
} catch {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* When the Host store is wired, read failures are retryable errors — never
|
|
297
|
+
* silent upstream-without-overlay (that would look like authoritative empty
|
|
298
|
+
* Host state). When the store is not wired, leave upstream untouched.
|
|
299
|
+
* @returns {{ status: 'unconfigured' | 'ok' | 'unavailable', stored?: object, error?: string }}
|
|
300
|
+
*/
|
|
301
|
+
const readStoredSessionMetadata = async () => {
|
|
302
|
+
if (!storeConfigured) return { status: 'unconfigured' };
|
|
303
|
+
try {
|
|
304
|
+
const stored = await getStoredSessionMetadata();
|
|
305
|
+
if (!stored || typeof stored !== 'object') {
|
|
306
|
+
return { status: 'unavailable', error: 'session metadata is unavailable' };
|
|
307
|
+
}
|
|
308
|
+
return { status: 'ok', stored };
|
|
309
|
+
} catch (error) {
|
|
310
|
+
const message = error instanceof Error ? error.message : 'session metadata is unavailable';
|
|
311
|
+
console.warn('[proxy] session metadata unavailable:', message);
|
|
312
|
+
return { status: 'unavailable', error: message };
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const readStoredSessionMetadataSync = () => {
|
|
317
|
+
if (typeof getStoredSessionMetadataSync !== 'function') {
|
|
318
|
+
return storeConfigured ? null : undefined;
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
const stored = getStoredSessionMetadataSync();
|
|
322
|
+
if (stored && typeof stored === 'object') return stored;
|
|
323
|
+
return null;
|
|
324
|
+
} catch {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const metadataUnavailableResponse = (res, error) => {
|
|
330
|
+
res.status(503).json({
|
|
331
|
+
error: error || 'session metadata is unavailable',
|
|
332
|
+
code: 'session_metadata_unavailable',
|
|
333
|
+
retryable: true,
|
|
334
|
+
});
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* OpenChamber's stored metadata deep-merges onto upstream create-time
|
|
339
|
+
* metadata, and Host archive authority projects onto `time.archived`.
|
|
340
|
+
*/
|
|
341
|
+
const overlaySession = (session, stored) => {
|
|
342
|
+
if (!stored) return session;
|
|
343
|
+
return projectSessionWithStoredMap(session, stored);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* @returns {Promise<{ ok: true, payload: unknown } | { ok: false, error: string }>}
|
|
348
|
+
*/
|
|
349
|
+
const overlayOwnedStateOnList = async (payload) => {
|
|
350
|
+
const records = sessionListRecords(payload);
|
|
351
|
+
if (!records) return { ok: true, payload };
|
|
352
|
+
const meta = await readStoredSessionMetadata();
|
|
353
|
+
if (meta.status === 'unconfigured') return { ok: true, payload };
|
|
354
|
+
if (meta.status === 'unavailable') return { ok: false, error: meta.error || 'session metadata is unavailable' };
|
|
355
|
+
const overlaid = records.map((session) => overlaySession(session, meta.stored));
|
|
356
|
+
// Preserve `{ data, cursor }` (and any other list envelope fields).
|
|
357
|
+
return {
|
|
358
|
+
ok: true,
|
|
359
|
+
payload: Array.isArray(payload) ? overlaid : { ...payload, data: overlaid },
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const createHostSessionSseFilter = () => ({
|
|
364
|
+
projectEvent: (payload) => {
|
|
365
|
+
// Transcript/message hot path stays open. Only session lifecycle frames
|
|
366
|
+
// require Host authority; until the store is ready they are suppressed.
|
|
367
|
+
const stored = readStoredSessionMetadataSync();
|
|
368
|
+
if (storeConfigured && stored == null) {
|
|
369
|
+
const type = payload?.type || payload?.payload?.type;
|
|
370
|
+
if (isSessionLifecycleEventType(type)) return null;
|
|
371
|
+
return payload;
|
|
372
|
+
}
|
|
373
|
+
if (!stored) return payload;
|
|
374
|
+
return projectSessionLifecyclePayload(payload, (sessionId) => {
|
|
375
|
+
const host = stored[sessionId];
|
|
376
|
+
return host && typeof host === 'object' && !Array.isArray(host) ? host : null;
|
|
377
|
+
}, { hostReady: true });
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const composeSseFilters = (...filters) => {
|
|
382
|
+
const active = filters.filter(Boolean);
|
|
383
|
+
if (active.length === 0) return null;
|
|
384
|
+
if (active.length === 1) return active[0];
|
|
385
|
+
return {
|
|
386
|
+
projectEvent: (payload) => {
|
|
387
|
+
let current = payload;
|
|
388
|
+
for (const filter of active) {
|
|
389
|
+
current = filter.projectEvent(current);
|
|
390
|
+
if (current == null) return null;
|
|
391
|
+
}
|
|
392
|
+
return current;
|
|
393
|
+
},
|
|
394
|
+
dispose: () => {
|
|
395
|
+
for (const filter of active) filter.dispose?.();
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
};
|
|
399
|
+
|
|
231
400
|
if (app.get('opencodeProxyConfigured')) {
|
|
232
401
|
return;
|
|
233
402
|
}
|
|
@@ -378,6 +547,15 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
378
547
|
};
|
|
379
548
|
|
|
380
549
|
const forwardSseRequest = async (req, res) => {
|
|
550
|
+
// Gate session-lifecycle Host authority before attaching upstream SSE so we
|
|
551
|
+
// never drop the only lifecycle frame while metadata is still unknown.
|
|
552
|
+
if (storeConfigured) {
|
|
553
|
+
const ready = await waitForHostMetadataReady();
|
|
554
|
+
if (!ready) {
|
|
555
|
+
return metadataUnavailableResponse(res, 'session metadata is unavailable');
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
381
559
|
const abortController = new AbortController();
|
|
382
560
|
const closeUpstream = () => abortController.abort();
|
|
383
561
|
let upstream = null;
|
|
@@ -395,7 +573,13 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
395
573
|
const includeReasoning = readIncludeReasoningQuery(req.query)
|
|
396
574
|
&& readIncludeReasoningFromUrl(requestUrl);
|
|
397
575
|
const reasoningFilter = includeReasoning ? null : createReasoningOutboundFilter();
|
|
398
|
-
const
|
|
576
|
+
const hostSessionFilter = typeof getStoredSessionMetadataSync === 'function'
|
|
577
|
+
? createHostSessionSseFilter()
|
|
578
|
+
: null;
|
|
579
|
+
const outboundSseFilter = composeSseFilters(reasoningFilter, hostSessionFilter);
|
|
580
|
+
// Parse SSE blocks when any Host projection filter is active (reasoning
|
|
581
|
+
// strip and/or session archive authority). Otherwise byte-identical passthrough.
|
|
582
|
+
const sseSplitter = outboundSseFilter ? createSseBlockSplitter() : null;
|
|
399
583
|
|
|
400
584
|
req.on('close', closeUpstream);
|
|
401
585
|
|
|
@@ -419,7 +603,7 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
419
603
|
try {
|
|
420
604
|
// Strip includeReasoning before building the upstream OpenCode path.
|
|
421
605
|
const strippedUrl = stripIncludeReasoningParam(requestUrl);
|
|
422
|
-
const upstreamPath =
|
|
606
|
+
const upstreamPath = toUpstreamOpenCodeApiPath(strippedUrl);
|
|
423
607
|
const headers = normalizeForwardedDirectoryHeaders(
|
|
424
608
|
collectForwardProxyHeaders(req.headers, getOpenCodeAuthHeaders())
|
|
425
609
|
);
|
|
@@ -511,20 +695,20 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
511
695
|
}
|
|
512
696
|
if (value && value.length > 0) {
|
|
513
697
|
resetStallTimer();
|
|
514
|
-
if (
|
|
515
|
-
//
|
|
698
|
+
if (!outboundSseFilter || !sseSplitter) {
|
|
699
|
+
// No Host projection: byte-identical passthrough.
|
|
516
700
|
sseBoundary.observe(value);
|
|
517
701
|
const canContinue = await enqueueSseWrite(value);
|
|
518
702
|
if (!canContinue) {
|
|
519
703
|
break;
|
|
520
704
|
}
|
|
521
705
|
} else {
|
|
522
|
-
//
|
|
523
|
-
// Preserves id/event lines and GlobalEvent wrap when unchanged
|
|
706
|
+
// Parse SSE blocks, apply reasoning strip + Host archive authority.
|
|
707
|
+
// Preserves id/event lines and GlobalEvent wrap when unchanged.
|
|
524
708
|
const blocks = sseSplitter.push(value);
|
|
525
709
|
let canContinue = true;
|
|
526
710
|
for (const block of blocks) {
|
|
527
|
-
const filtered = filterSseBlock(block,
|
|
711
|
+
const filtered = filterSseBlock(block, outboundSseFilter);
|
|
528
712
|
if (!filtered) {
|
|
529
713
|
continue;
|
|
530
714
|
}
|
|
@@ -539,9 +723,9 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
539
723
|
}
|
|
540
724
|
}
|
|
541
725
|
|
|
542
|
-
if (
|
|
726
|
+
if (outboundSseFilter && sseSplitter) {
|
|
543
727
|
for (const block of sseSplitter.finish()) {
|
|
544
|
-
const filtered = filterSseBlock(block,
|
|
728
|
+
const filtered = filterSseBlock(block, outboundSseFilter);
|
|
545
729
|
if (filtered) {
|
|
546
730
|
sseBoundary.observe(filtered);
|
|
547
731
|
await enqueueSseWrite(filtered);
|
|
@@ -570,7 +754,7 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
570
754
|
} finally {
|
|
571
755
|
clearConnectTimer();
|
|
572
756
|
clearStallTimer();
|
|
573
|
-
|
|
757
|
+
outboundSseFilter?.dispose?.();
|
|
574
758
|
if (heartbeatTimer) {
|
|
575
759
|
clearTimeout(heartbeatTimer);
|
|
576
760
|
heartbeatTimer = null;
|
|
@@ -588,7 +772,7 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
588
772
|
}
|
|
589
773
|
};
|
|
590
774
|
|
|
591
|
-
const fetchSessionListPayload = async (upstreamPath, { req = null, timeoutMs = null } = {}) => {
|
|
775
|
+
const fetchSessionListPayload = async (upstreamPath, { req = null, timeoutMs = null, method = 'GET' } = {}) => {
|
|
592
776
|
const headers = req
|
|
593
777
|
? {
|
|
594
778
|
...normalizeForwardedDirectoryHeaders(collectForwardProxyHeaders(req.headers, getOpenCodeAuthHeaders())),
|
|
@@ -601,7 +785,7 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
601
785
|
'accept-encoding': 'identity',
|
|
602
786
|
};
|
|
603
787
|
const upstream = await fetch(buildOpenCodeUrl(upstreamPath, ''), {
|
|
604
|
-
method: 'GET',
|
|
788
|
+
method: method || 'GET',
|
|
605
789
|
headers,
|
|
606
790
|
...(typeof timeoutMs === 'number' ? { signal: AbortSignal.timeout(timeoutMs) } : {}),
|
|
607
791
|
});
|
|
@@ -625,8 +809,7 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
625
809
|
const requestUrl = typeof req.originalUrl === 'string' && req.originalUrl.length > 0
|
|
626
810
|
? req.originalUrl
|
|
627
811
|
: (typeof req.url === 'string' ? req.url : '');
|
|
628
|
-
|
|
629
|
-
return canonicalizeDirectoryQuery(upstreamPathRaw);
|
|
812
|
+
return canonicalizeDirectoryQuery(toUpstreamOpenCodeApiPath(requestUrl));
|
|
630
813
|
};
|
|
631
814
|
|
|
632
815
|
const forwardSanitizedSessionListRequest = async (req, res, next, logLabel) => {
|
|
@@ -643,14 +826,18 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
643
826
|
return;
|
|
644
827
|
}
|
|
645
828
|
|
|
646
|
-
if (result.parseError || !
|
|
829
|
+
if (result.parseError || !sessionListRecords(result.payload)) {
|
|
647
830
|
res.setHeader('content-type', result.contentType);
|
|
648
831
|
res.end(result.bodyText);
|
|
649
832
|
return;
|
|
650
833
|
}
|
|
651
834
|
|
|
652
835
|
res.setHeader('content-type', result.contentType);
|
|
653
|
-
|
|
836
|
+
const overlaid = await overlayOwnedStateOnList(sanitizeSessionListPayload(result.payload));
|
|
837
|
+
if (!overlaid.ok) {
|
|
838
|
+
return metadataUnavailableResponse(res, overlaid.error);
|
|
839
|
+
}
|
|
840
|
+
res.json(overlaid.payload);
|
|
654
841
|
} catch (error) {
|
|
655
842
|
if (isAbortError(error)) {
|
|
656
843
|
return;
|
|
@@ -735,7 +922,7 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
735
922
|
const READINESS_HOLD_POLL_MS = 75;
|
|
736
923
|
const READINESS_HOLD_MAX_MS = 6000;
|
|
737
924
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
738
|
-
const
|
|
925
|
+
const isProcessWarmingUp = (runtimeState) => {
|
|
739
926
|
const waitElapsed = runtimeState.openCodeNotReadySince === 0 ? 0 : Date.now() - runtimeState.openCodeNotReadySince;
|
|
740
927
|
return (
|
|
741
928
|
(!runtimeState.isOpenCodeReady && (runtimeState.openCodeNotReadySince === 0 || waitElapsed < OPEN_CODE_READY_GRACE_MS)) ||
|
|
@@ -744,6 +931,18 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
744
931
|
);
|
|
745
932
|
};
|
|
746
933
|
|
|
934
|
+
// Process warmup keeps the existing grace window. V1 migration is separate:
|
|
935
|
+
// admitTranscript=false holds with no grace until the gate admits or the
|
|
936
|
+
// client cancels. After grace, a still-running migration must not next()
|
|
937
|
+
// an empty session list through as an authoritative success.
|
|
938
|
+
const isMigrationBlockingTranscript = (runtimeState) => (
|
|
939
|
+
runtimeState?.v1Migration != null && runtimeState.v1Migration.admitTranscript !== true
|
|
940
|
+
);
|
|
941
|
+
|
|
942
|
+
const shouldHoldProxiedRequest = (runtimeState) => (
|
|
943
|
+
isProcessWarmingUp(runtimeState) || isMigrationBlockingTranscript(runtimeState)
|
|
944
|
+
);
|
|
945
|
+
|
|
747
946
|
app.use('/api', async (req, res, next) => {
|
|
748
947
|
if (
|
|
749
948
|
req.path.startsWith('/themes/custom') ||
|
|
@@ -753,23 +952,62 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
753
952
|
req.path.startsWith('/config/settings') ||
|
|
754
953
|
req.path.startsWith('/config/skills') ||
|
|
755
954
|
req.path === '/config/reload' ||
|
|
756
|
-
req.path === '/health'
|
|
955
|
+
req.path === '/health' ||
|
|
956
|
+
req.path === '/opencode/health' ||
|
|
957
|
+
req.path === '/opencode/version' ||
|
|
958
|
+
req.path === '/opencode/contract' ||
|
|
959
|
+
req.path === '/opencode/upgrade-status' ||
|
|
960
|
+
req.path === '/opencode/upgrade'
|
|
757
961
|
) {
|
|
758
962
|
return next();
|
|
759
963
|
}
|
|
760
964
|
|
|
761
|
-
|
|
965
|
+
// Ticket 11: limit execution when the running serve fails contract admission.
|
|
966
|
+
// Diagnostics, reads, and stop-task stay available; health success alone is not full semantics.
|
|
967
|
+
// Require explicit executionAllowed === true for the *current* instance (null/pending blocks).
|
|
968
|
+
// Gate again after readiness hold so initial-unknown → ready-incompatible cannot slip through.
|
|
969
|
+
const pathForGate = req.originalUrl || req.url || req.path;
|
|
970
|
+
const gateOptions = typeof getRuntimeContractGateOptions === 'function'
|
|
971
|
+
? getRuntimeContractGateOptions()
|
|
972
|
+
: {};
|
|
973
|
+
const blockedByContract = () => {
|
|
974
|
+
const contract = getRuntime()?.runtimeContract;
|
|
975
|
+
if (shouldBlockRuntimeContractExecution(req.method, pathForGate, contract, gateOptions)) {
|
|
976
|
+
return runtimeContractExecutionBlockedBody(contract);
|
|
977
|
+
}
|
|
978
|
+
return null;
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
const earlyBlock = blockedByContract();
|
|
982
|
+
if (earlyBlock) {
|
|
983
|
+
return res.status(409).json(earlyBlock);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (!shouldHoldProxiedRequest(getRuntime())) {
|
|
987
|
+
const lateBlock = blockedByContract();
|
|
988
|
+
if (lateBlock) {
|
|
989
|
+
return res.status(409).json(lateBlock);
|
|
990
|
+
}
|
|
762
991
|
return next();
|
|
763
992
|
}
|
|
764
993
|
|
|
765
|
-
const
|
|
766
|
-
while (
|
|
994
|
+
const processDeadline = Date.now() + Math.min(OPEN_CODE_READY_GRACE_MS, READINESS_HOLD_MAX_MS);
|
|
995
|
+
while (true) {
|
|
767
996
|
// Client gave up (closed/aborted) — stop holding.
|
|
768
997
|
if (res.writableEnded || req.aborted) return;
|
|
769
|
-
|
|
770
|
-
if (!
|
|
998
|
+
const runtimeState = getRuntime();
|
|
999
|
+
if (!shouldHoldProxiedRequest(runtimeState)) {
|
|
1000
|
+
// Re-evaluate after wait: startup may have published incompatible/pending.
|
|
1001
|
+
const afterHoldBlock = blockedByContract();
|
|
1002
|
+
if (afterHoldBlock) {
|
|
1003
|
+
return res.status(409).json(afterHoldBlock);
|
|
1004
|
+
}
|
|
771
1005
|
return next();
|
|
772
1006
|
}
|
|
1007
|
+
if (!isMigrationBlockingTranscript(runtimeState) && Date.now() >= processDeadline) {
|
|
1008
|
+
break;
|
|
1009
|
+
}
|
|
1010
|
+
await sleep(READINESS_HOLD_POLL_MS);
|
|
773
1011
|
}
|
|
774
1012
|
|
|
775
1013
|
if (!res.headersSent) {
|
|
@@ -852,7 +1090,11 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
852
1090
|
return bTime - aTime;
|
|
853
1091
|
});
|
|
854
1092
|
console.log(`[SessionMerge] ${globalSessions?.length || 0} global + ${extraSessions.length} extra = ${merged.length} total`);
|
|
855
|
-
|
|
1093
|
+
const overlaid = await overlayOwnedStateOnList(sanitizeSessionListPayload(merged));
|
|
1094
|
+
if (!overlaid.ok) {
|
|
1095
|
+
return metadataUnavailableResponse(res, overlaid.error);
|
|
1096
|
+
}
|
|
1097
|
+
return res.json(overlaid.payload);
|
|
856
1098
|
} catch (error) {
|
|
857
1099
|
console.log(`[SessionMerge] Error: ${error.message}`);
|
|
858
1100
|
return res.status(500).json({ error: error.message || 'Failed to merge Windows sessions' });
|
|
@@ -864,6 +1106,102 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
864
1106
|
return forwardSanitizedSessionListRequest(req, res, next, 'session.list');
|
|
865
1107
|
});
|
|
866
1108
|
|
|
1109
|
+
// One session: the same metadata overlay as the list, so a detail read agrees
|
|
1110
|
+
// with the list it came from. Everything else about the record is forwarded
|
|
1111
|
+
// untouched. Without a store, fall through to the generic proxy.
|
|
1112
|
+
app.get('/api/session/:sessionID', async (req, res, next) => {
|
|
1113
|
+
if (!storeConfigured) return next();
|
|
1114
|
+
// Nested routes (message, children, …) are registered separately; Express
|
|
1115
|
+
// still matches this pattern for bare session ids only when no further
|
|
1116
|
+
// segment is present — but be defensive if a child path slips through.
|
|
1117
|
+
if (req.path.includes('/message') || String(req.params.sessionID || '').includes('/')) {
|
|
1118
|
+
return next();
|
|
1119
|
+
}
|
|
1120
|
+
try {
|
|
1121
|
+
const upstreamPath = await getRequestUpstreamPath(req);
|
|
1122
|
+
const result = await fetchSessionListPayload(upstreamPath, { req });
|
|
1123
|
+
|
|
1124
|
+
const record = result.isJson && !result.parseError ? result.payload : null;
|
|
1125
|
+
const session = record && typeof record === 'object' && !Array.isArray(record)
|
|
1126
|
+
? (record.data && typeof record.data === 'object' && !Array.isArray(record.data) ? record.data : record)
|
|
1127
|
+
: null;
|
|
1128
|
+
if (!session || typeof session.id !== 'string') {
|
|
1129
|
+
res.status(result.upstream.status);
|
|
1130
|
+
applyForwardProxyResponseHeaders(result.upstream.headers, res);
|
|
1131
|
+
res.setHeader('content-type', result.contentType);
|
|
1132
|
+
res.end(result.bodyText);
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const meta = await readStoredSessionMetadata();
|
|
1137
|
+
if (meta.status === 'unavailable') {
|
|
1138
|
+
return metadataUnavailableResponse(res, meta.error);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
res.status(result.upstream.status);
|
|
1142
|
+
applyForwardProxyResponseHeaders(result.upstream.headers, res);
|
|
1143
|
+
res.setHeader('content-type', result.contentType);
|
|
1144
|
+
|
|
1145
|
+
if (meta.status === 'unconfigured') {
|
|
1146
|
+
res.end(result.bodyText);
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const overlaid = overlaySession(session, meta.stored);
|
|
1151
|
+
res.json(record.data && typeof record.data === 'object' && !Array.isArray(record.data)
|
|
1152
|
+
? { ...record, data: overlaid }
|
|
1153
|
+
: overlaid);
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
if (isAbortError(error)) return;
|
|
1156
|
+
console.error('[proxy] OpenCode session.get proxy error:', error?.message ?? error);
|
|
1157
|
+
if (!res.headersSent) {
|
|
1158
|
+
res.status(503).json({ error: 'OpenCode service unavailable' });
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
next(error);
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
// Successful upstream delete → idempotent Host metadata cleanup. Failures are
|
|
1166
|
+
// observable (logged) and compensated by session.deleted event ingest.
|
|
1167
|
+
app.delete('/api/session/:sessionID', async (req, res, next) => {
|
|
1168
|
+
if (typeof onSessionDeleted !== 'function') return next();
|
|
1169
|
+
const sessionID = typeof req.params?.sessionID === 'string' ? req.params.sessionID.trim() : '';
|
|
1170
|
+
if (!sessionID || sessionID.includes('/')) return next();
|
|
1171
|
+
try {
|
|
1172
|
+
const upstreamPath = await getRequestUpstreamPath(req);
|
|
1173
|
+
const result = await fetchSessionListPayload(upstreamPath, {
|
|
1174
|
+
req,
|
|
1175
|
+
method: 'DELETE',
|
|
1176
|
+
});
|
|
1177
|
+
res.status(result.upstream.status);
|
|
1178
|
+
applyForwardProxyResponseHeaders(result.upstream.headers, res);
|
|
1179
|
+
res.setHeader('content-type', result.contentType || 'application/json');
|
|
1180
|
+
if (result.upstream.status >= 200 && result.upstream.status < 300) {
|
|
1181
|
+
try {
|
|
1182
|
+
const cleanup = onSessionDeleted(sessionID);
|
|
1183
|
+
if (cleanup && typeof cleanup.catch === 'function') {
|
|
1184
|
+
cleanup.catch((error) => {
|
|
1185
|
+
console.warn('[proxy] session metadata cleanup after delete failed (retry via event):', error?.message ?? error);
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
console.warn('[proxy] session metadata cleanup after delete failed (retry via event):', error?.message ?? error);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
// Upstream failure keeps Host metadata (do not cleanup).
|
|
1193
|
+
res.end(result.bodyText);
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
if (isAbortError(error)) return;
|
|
1196
|
+
console.error('[proxy] OpenCode session.delete proxy error:', error?.message ?? error);
|
|
1197
|
+
if (!res.headersSent) {
|
|
1198
|
+
res.status(503).json({ error: 'OpenCode service unavailable' });
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
next(error);
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
|
|
867
1205
|
// Official session.messages list (not exact message — that is owned by
|
|
868
1206
|
// session-turn-pages and registered before this proxy).
|
|
869
1207
|
app.get('/api/session/:sessionID/message', (req, res, next) => {
|
|
@@ -881,7 +1219,9 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
881
1219
|
const apiProxy = createProxyMiddleware({
|
|
882
1220
|
target: resolveProxyTarget(),
|
|
883
1221
|
changeOrigin: true,
|
|
884
|
-
|
|
1222
|
+
// Express already stripped the `/api` mount. Restore it so v2 receives
|
|
1223
|
+
// `/api/agent` rather than `/agent` (which serves the Web UI HTML).
|
|
1224
|
+
pathRewrite: (pathValue) => toUpstreamOpenCodeApiPath(pathValue),
|
|
885
1225
|
timeout: PROXY_REQUEST_TIMEOUT_MS,
|
|
886
1226
|
proxyTimeout: PROXY_REQUEST_TIMEOUT_MS,
|
|
887
1227
|
// Dynamic target — port can change after restart
|