@openchambery/web 2.0.0-beta.1 → 2.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -11
- package/bin/cli-opencode.test.js +4 -4
- package/bin/cli.js +1 -1
- package/bin/lib/cli-startup.js +1 -1
- package/bin/lib/cli-startup.test.js +18 -4
- package/dist/assets/{AgentAvatar-CLWWNu_Q.js → AgentAvatar-Q1SmFRx2.js} +1 -1
- package/dist/assets/AssistantView-CwXkiCAd.js +3 -0
- package/dist/assets/{DiagramView-xuSJTnv7.js → DiagramView-BW92schO.js} +1 -1
- package/dist/assets/{FadeInOnReveal-Ov0D96d0.js → FadeInOnReveal-BZmwl-zE.js} +1 -1
- package/dist/assets/{Icon-DD-BuMfZ.js → Icon-AQPvxMiU.js} +1 -1
- package/dist/assets/{MarkdownRenderer-bc_p1H7l.js → MarkdownRenderer-Bv05iDZZ.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-SKi4oG0c.js → MarkdownRendererImpl-C81xqYib.js} +3 -3
- package/dist/assets/MarkstreamRendererImpl-CXUCQuKJ.js +3 -0
- package/dist/assets/{MobileDetailNavigation-Bnx59U_J.js → MobileDetailNavigation-BMgIu7zc.js} +1 -1
- package/dist/assets/{MobileShareBridge-BQlP4J-F.js → MobileShareBridge-BOXGNHmC.js} +2 -2
- package/dist/assets/MobileSurface-xuc_z0_G.js +2 -0
- package/dist/assets/{MultiRunWindow-ChZGob0v.js → MultiRunWindow-B5At2Z95.js} +1 -1
- package/dist/assets/{OnboardingScreen-DtQJxZG-.js → OnboardingScreen-bsPLgz4r.js} +1 -1
- package/dist/assets/OverlayScrollbar-4wp4migh.js +2 -0
- package/dist/assets/SettingsView-CJ6tcH5_.js +25 -0
- package/dist/assets/{SettingsWindow-DfnsFa8y.js → SettingsWindow-CMtX2bMl.js} +1 -1
- package/dist/assets/{TerminalView-Dh9MWCpS.js → TerminalView-Dvr-ysTS.js} +1 -1
- package/dist/assets/{ToolOutputDialog-BejmjrRk.js → ToolOutputDialog-uH1ZZDSS.js} +2 -2
- package/dist/assets/appThemeRegistry-DK7q2juD.js +1 -0
- package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
- package/dist/assets/{button-D-rATN7w.js → button-CcNJUxUd.js} +1 -1
- package/dist/assets/collapsible-ZmZ6rLWs.js +3 -0
- package/dist/assets/context-menu-B2LXJgXL.js +1 -0
- package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-C6HrP0jq.js} +1 -1
- package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DdVBp45U.js} +1 -1
- package/dist/assets/{diagram-DQf7Y_uR.js → diagram-CxTs9hCQ.js} +1 -1
- package/dist/assets/{dialog-bfbvR4VH.js → dialog-KMVL9QKF.js} +1 -1
- package/dist/assets/es-CN3E_zXj.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-BvrNCroG.js → fileMentionTouchSelection-7RIwlpkB.js} +1 -1
- package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-CjdBRy8y.js} +1 -1
- package/dist/assets/fr-9zZvQZD2.js +13 -0
- package/dist/assets/gitApi-yuWg9BN4.js +10 -0
- package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-9vQBq3zp.js} +1 -1
- package/dist/assets/{input-8r6F_Fza.js → input-CrIvk3HZ.js} +1 -1
- package/dist/assets/insertionBoundaries-BiUCSkx-.js +1 -0
- package/dist/assets/ja-Dnjk_82N.js +15 -0
- package/dist/assets/ko-BxCEE99C.js +15 -0
- package/dist/assets/{main-CCE_4y5F.js → main-C9mIBo77.js} +2 -2
- package/dist/assets/main-DiL6w6KF.js +169 -0
- package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
- package/dist/assets/{markdownHeightCache-1Qo6y6IJ.js → markdownHeightCache-BmXVY_gc.js} +1 -1
- package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
- package/dist/assets/mcpQueries-C7jknHV8.js +1 -0
- package/dist/assets/miniChat-CwtUaElU.js +2 -0
- package/dist/assets/mobile-DB-6--ws.js +2 -0
- package/dist/assets/multirun-TAsxFI8e.js +4 -0
- package/dist/assets/{persistence-Ce3AnpEr.js → persistence-56kq8Z4V.js} +2 -2
- package/dist/assets/pl-CNNsWnPM.js +15 -0
- package/dist/assets/platform-C13fMVEs.js +6 -0
- package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
- package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-CGj0MNRU.js +2 -0
- package/dist/assets/renderMobileApp-CGMMqbsO.js +28 -0
- package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
- package/dist/assets/{runtimeConfig-BMvqa5we.js → runtimeConfig-BEANkvOd.js} +1 -1
- package/dist/assets/runtimeEndpointReset-A_ZBUJFS.js +147 -0
- package/dist/assets/sessionChangeSummary-qOLrP9fw.js +2 -0
- package/dist/assets/{sessionLookup-gh-hVRIc.js → sessionLookup-34XFmP9C.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-CudoSgV4.js → sortable-tabs-strip-DyEV0Heg.js} +1 -1
- package/dist/assets/src-ubCWXZhR.css +2 -0
- package/dist/assets/store-DLCiZaA6.js +15 -0
- package/dist/assets/{terminalApi-DiF6JmBP.js → terminalApi-CXaBEoa_.js} +1 -1
- package/dist/assets/textarea-BBOlRI9O.js +1 -0
- package/dist/assets/{tooltip-CQvpK-UV.js → tooltip-CPI4fr1a.js} +1 -1
- package/dist/assets/uk-rwQrxeYM.js +15 -0
- package/dist/assets/{url-_z7evzdK.js → url-DRDQXjyX.js} +1 -1
- package/dist/assets/{useAppFontEffects-Cpmpo7Wb.js → useAppFontEffects-CiDe42I5.js} +57 -43
- package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-BlizAlOt.js +1 -0
- package/dist/assets/useEffectiveDirectory-C_CouL5v.js +1 -0
- package/dist/assets/{useFontPreferences-DcbXQOkc.js → useFontPreferences-B0oUZAiB.js} +1 -1
- package/dist/assets/useMobileNavigationStore-DfPClaW-.js +414 -0
- package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
- package/dist/assets/useSessionAutoCleanup-DtrfbxAp.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-Bqn4AiBL.js → useWorkerHighlightedLines-CPNYz6Wf.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-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-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 +1 -1
- package/server/index.js +327 -62
- package/server/lib/assistants/DOCUMENTATION.md +14 -9
- package/server/lib/assistants/contracts.js +11 -1
- package/server/lib/assistants/projection-history.test.js +265 -18
- package/server/lib/assistants/routes.js +6 -0
- package/server/lib/assistants/service.js +417 -231
- package/server/lib/assistants/service.test.js +392 -148
- package/server/lib/conversations/DOCUMENTATION.md +1 -1
- package/server/lib/conversations/service.js +1 -0
- package/server/lib/conversations/service.test.js +16 -0
- package/server/lib/conversations/validation.js +11 -1
- package/server/lib/event-stream/DOCUMENTATION.md +3 -1
- package/server/lib/event-stream/directory-ws-bridge.js +13 -2
- package/server/lib/event-stream/global-hub.js +49 -4
- package/server/lib/event-stream/reasoning-projection.js +16 -1
- package/server/lib/event-stream/reasoning-projection.test.js +15 -0
- 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 +33 -9
- package/server/lib/llm/catalog.js +1 -1
- package/server/lib/llm/completions.js +5 -1
- package/server/lib/llm/completions.test.js +3 -3
- package/server/lib/llm/generate.js +237 -145
- package/server/lib/llm/generate.test.js +201 -46
- package/server/lib/llm/routes.js +2 -0
- package/server/lib/message-queue/runtime.js +15 -2
- package/server/lib/notifications/runtime.js +23 -5
- package/server/lib/notifications/runtime.test.js +11 -0
- package/server/lib/opencode/DOCUMENTATION.md +85 -17
- 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 +42 -99
- package/server/lib/opencode/config-entity-routes.test.js +107 -0
- package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
- package/server/lib/opencode/ensure-cli.js +1 -2
- package/server/lib/opencode/env-runtime.js +40 -26
- package/server/lib/opencode/env-runtime.test.js +10 -17
- package/server/lib/opencode/feature-routes-runtime.js +43 -4
- 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 +349 -39
- package/server/lib/opencode/lifecycle.test.js +322 -0
- 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/openchamber-routes.js +7 -1
- package/server/lib/opencode/opencode2-pin.js +4 -4
- package/server/lib/opencode/opencode2-pin.test.js +1 -0
- package/server/lib/opencode/opencode2-sidecar.e2e.test.js +127 -4
- 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-routes.js +3 -18
- package/server/lib/opencode/plugin-routes.test.js +9 -9
- 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 +252 -46
- package/server/lib/opencode/proxy.test.js +235 -2
- package/server/lib/opencode/routes.js +296 -62
- package/server/lib/opencode/routes.test.js +278 -12
- 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 +33 -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 +20 -33
- package/server/lib/opencode/skill-routes.test.js +35 -2
- 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/v2-client.js +14 -3
- 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/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 +8 -0
- package/server/lib/scheduled-tasks/runtime.js +52 -4
- package/server/lib/scheduled-tasks/runtime.test.js +80 -1
- package/server/lib/session-goal/DOCUMENTATION.md +113 -13
- package/server/lib/session-goal/routes.js +9 -0
- package/server/lib/session-goal/runtime.js +1579 -117
- package/server/lib/session-goal/runtime.test.js +1453 -84
- 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 +161 -34
- package/server/lib/session-index/sync-runtime.test.js +155 -1
- package/server/lib/session-metadata/DOCUMENTATION.md +61 -16
- package/server/lib/session-metadata/routes.js +108 -9
- package/server/lib/session-metadata/routes.test.js +47 -2
- 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 +212 -72
- package/server/lib/session-metadata/session-metadata-store.test.js +205 -10
- 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 +9 -8
- package/server/lib/session-turn-pages/routes.js +17 -102
- package/server/lib/session-turn-pages/routes.test.js +281 -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 -147
- package/server/lib/small-model/catalog.js +31 -51
- package/server/lib/small-model/catalog.test.js +65 -10
- 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 +124 -240
- 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 +2 -0
- package/dist/assets/AssistantView-BTCkm8yk.js +0 -3
- package/dist/assets/MarkstreamRendererImpl-CnkodGJW.js +0 -3
- package/dist/assets/MobileSurface-BXLeiZ4d.js +0 -2
- package/dist/assets/OverlayScrollbar-pMwxaCca.js +0 -2
- package/dist/assets/SettingsView-B5I0KDTZ.js +0 -25
- package/dist/assets/appThemeRegistry-COGfxTFV.js +0 -1
- package/dist/assets/collapsible-BXRd5_Hr.js +0 -1
- package/dist/assets/context-menu-jGtH6fR9.js +0 -1
- package/dist/assets/es-c52LTHHe.js +0 -15
- package/dist/assets/fr-PisFbUE8.js +0 -13
- package/dist/assets/gitApi-ButwzQ0N.js +0 -9
- package/dist/assets/insertionBoundaries-DGs7S5zv.js +0 -1
- package/dist/assets/ja-C8Ikun3Z.js +0 -15
- package/dist/assets/ko-ClFys5o2.js +0 -15
- package/dist/assets/main-Dn9rpsgl.js +0 -169
- package/dist/assets/markdown-worker-CptoICmq.js +0 -2
- package/dist/assets/miniChat-BpjxVqgg.js +0 -2
- package/dist/assets/mobile-nMJCaNq1.js +0 -2
- package/dist/assets/multirun-BvSG94mW.js +0 -4
- package/dist/assets/pl-CnaKWN5F.js +0 -15
- package/dist/assets/platform-CWTIp02q.js +0 -4
- package/dist/assets/projectMeta-DOe49dM_.js +0 -3
- package/dist/assets/pt-BR-s8OpfRI6.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +0 -2
- package/dist/assets/renderMobileApp-y-S8y8v_.js +0 -28
- package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
- package/dist/assets/runtimeEndpointReset-B6BxhvR0.js +0 -147
- package/dist/assets/sessionChangeSummary-VVBlEXuh.js +0 -2
- package/dist/assets/src-BzX5UI-c.css +0 -2
- package/dist/assets/store-DJczmczv.js +0 -15
- package/dist/assets/textarea-3XR8sUfy.js +0 -1
- package/dist/assets/uk-CaX0iqkB.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +0 -1
- package/dist/assets/useEffectiveDirectory-CN6JoCFw.js +0 -1
- package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +0 -414
- package/dist/assets/useSessionAutoCleanup-BLjymaFt.js +0 -2
- package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
- package/dist/assets/zh-CN-20MYEjha.js +0 -15
- package/dist/assets/zh-TW-DiN04qTw.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 -183
- package/server/lib/small-model/opencode-session.test.js +0 -147
|
@@ -56,20 +56,37 @@ const projectModalities = (source) => {
|
|
|
56
56
|
return ownKeys(projected).length > 0 ? projected : undefined;
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
+
// v2 ModelCapabilities.input/output are modality name arrays, not boolean maps.
|
|
60
|
+
const projectModalityList = (source) => {
|
|
61
|
+
if (!Array.isArray(source)) return undefined;
|
|
62
|
+
const projected = createDictionary();
|
|
63
|
+
for (const item of source) {
|
|
64
|
+
if (typeof item !== 'string' || !MODALITIES.has(item) || !isSafeIdentifier(item)) continue;
|
|
65
|
+
projected[item] = true;
|
|
66
|
+
}
|
|
67
|
+
return ownKeys(projected).length > 0 ? projected : undefined;
|
|
68
|
+
};
|
|
69
|
+
|
|
59
70
|
const projectCapabilities = (source) => {
|
|
60
71
|
if (!isRecord(source)) return undefined;
|
|
61
72
|
const capabilities = createDictionary();
|
|
62
73
|
const flags = projectBooleanFields(source, ['temperature', 'reasoning', 'attachment', 'toolcall']);
|
|
63
74
|
if (flags) Object.assign(capabilities, flags);
|
|
75
|
+
// v2 names tool support `tools`. Keep an explicit catalog `toolcall` flag when both exist.
|
|
76
|
+
if (!Object.hasOwn(capabilities, 'toolcall') && typeof source.tools === 'boolean') {
|
|
77
|
+
capabilities.toolcall = source.tools;
|
|
78
|
+
}
|
|
64
79
|
for (const field of ['input', 'output']) {
|
|
65
80
|
if (!Object.hasOwn(source, field)) continue;
|
|
66
|
-
const modalities =
|
|
81
|
+
const modalities = Array.isArray(source[field])
|
|
82
|
+
? projectModalityList(source[field])
|
|
83
|
+
: projectModalities(source[field]);
|
|
67
84
|
if (modalities) capabilities[field] = modalities;
|
|
68
85
|
}
|
|
69
86
|
return ownKeys(capabilities).length > 0 ? capabilities : undefined;
|
|
70
87
|
};
|
|
71
88
|
|
|
72
|
-
const
|
|
89
|
+
const projectCostEntry = (source) => {
|
|
73
90
|
if (!isRecord(source)) return undefined;
|
|
74
91
|
const cost = createDictionary();
|
|
75
92
|
for (const field of ['input', 'output']) {
|
|
@@ -87,6 +104,30 @@ const projectCost = (source) => {
|
|
|
87
104
|
return ownKeys(cost).length > 0 ? cost : undefined;
|
|
88
105
|
};
|
|
89
106
|
|
|
107
|
+
// v2 ModelInfo.cost is a tier array. The untiered entry is the base price.
|
|
108
|
+
const projectCost = (source) => {
|
|
109
|
+
if (Array.isArray(source)) {
|
|
110
|
+
const base = source.find((item) => isRecord(item) && !isRecord(item.tier))
|
|
111
|
+
|| source.find((item) => isRecord(item));
|
|
112
|
+
return projectCostEntry(base);
|
|
113
|
+
}
|
|
114
|
+
return projectCostEntry(source);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// v2 ModelInfo.variants is `{ id, settings?, headers?, body? }[]`. Catalog consumers
|
|
118
|
+
// still read a name→{} record. Request payloads stay server-side.
|
|
119
|
+
const projectVariantNames = (source) => {
|
|
120
|
+
if (Array.isArray(source)) {
|
|
121
|
+
return source.map((item) => {
|
|
122
|
+
if (typeof item === 'string') return item;
|
|
123
|
+
if (isRecord(item) && typeof item.id === 'string') return item.id;
|
|
124
|
+
return undefined;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (isRecord(source)) return ownKeys(source);
|
|
128
|
+
return null;
|
|
129
|
+
};
|
|
130
|
+
|
|
90
131
|
function projectModel(source) {
|
|
91
132
|
if (!isRecord(source) || !isSafeIdentifier(source.id) || !isSafeDisplayName(source.name)) return null;
|
|
92
133
|
// partial is structural only (e.g. variant truncation). Soft allowlist stripping of optional
|
|
@@ -113,18 +154,23 @@ function projectModel(source) {
|
|
|
113
154
|
if (Object.hasOwn(source, 'release_date') && source.release_date !== '' && source.release_date !== null) {
|
|
114
155
|
if (isSafeReleaseDate(source.release_date)) model.release_date = source.release_date;
|
|
115
156
|
}
|
|
116
|
-
if (Object.hasOwn(source, 'variants')
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
157
|
+
if (Object.hasOwn(source, 'variants')) {
|
|
158
|
+
const names = projectVariantNames(source.variants);
|
|
159
|
+
if (names) {
|
|
160
|
+
const variants = createDictionary();
|
|
161
|
+
let variantCount = 0;
|
|
162
|
+
for (const variantName of names) {
|
|
163
|
+
if (variantCount >= MAX_VARIANTS_PER_MODEL) {
|
|
164
|
+
partial = true;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
variantCount += 1;
|
|
168
|
+
if (typeof variantName === 'string' && isSafeIdentifier(variantName) && !Object.hasOwn(variants, variantName)) {
|
|
169
|
+
variants[variantName] = createDictionary();
|
|
170
|
+
}
|
|
123
171
|
}
|
|
124
|
-
|
|
125
|
-
if (isSafeIdentifier(variantName)) variants[variantName] = createDictionary();
|
|
172
|
+
if (ownKeys(variants).length > 0) model.variants = variants;
|
|
126
173
|
}
|
|
127
|
-
if (ownKeys(variants).length > 0) model.variants = variants;
|
|
128
174
|
}
|
|
129
175
|
return { value: model, partial };
|
|
130
176
|
}
|
|
@@ -204,6 +204,54 @@ describe('projectProviderCatalog', () => {
|
|
|
204
204
|
expect(result.value.partial).toBe(true);
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
+
it('projects v2 variant arrays, capability lists, and tiered cost without leaking request payloads', () => {
|
|
208
|
+
const result = projectProviderCatalog({
|
|
209
|
+
providers: [{
|
|
210
|
+
id: 'openai',
|
|
211
|
+
name: 'OpenAI',
|
|
212
|
+
models: {
|
|
213
|
+
'gpt-5': {
|
|
214
|
+
id: 'gpt-5',
|
|
215
|
+
name: 'GPT 5',
|
|
216
|
+
capabilities: {
|
|
217
|
+
tools: true,
|
|
218
|
+
input: ['text', 'image', 'secret-modality'],
|
|
219
|
+
output: ['text'],
|
|
220
|
+
},
|
|
221
|
+
cost: [
|
|
222
|
+
{ tier: { type: 'context', size: 200000 }, input: 9, output: 9, cache: { read: 1, write: 1 } },
|
|
223
|
+
{ input: 1, output: 2, cache: { read: 0.1, write: 0.2 }, secret: 'cost-sentinel' },
|
|
224
|
+
],
|
|
225
|
+
variants: [
|
|
226
|
+
{ id: 'low', settings: { reasoningEffort: 'low', apiKey: 'variant-settings-sentinel' }, headers: { Authorization: 'variant-headers-sentinel' } },
|
|
227
|
+
{ id: 'high', body: { store: false } },
|
|
228
|
+
{ id: 'low' },
|
|
229
|
+
{ id: 'constructor' },
|
|
230
|
+
'max',
|
|
231
|
+
{ settings: { token: 'variant-missing-id-sentinel' } },
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
}],
|
|
236
|
+
default: {},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
expect(result.ok).toBe(true);
|
|
240
|
+
expect(result.value.partial).toBe(false);
|
|
241
|
+
expect(result.value.providers[0].models['gpt-5']).toEqual({
|
|
242
|
+
id: 'gpt-5',
|
|
243
|
+
name: 'GPT 5',
|
|
244
|
+
capabilities: {
|
|
245
|
+
toolcall: true,
|
|
246
|
+
input: { text: true, image: true },
|
|
247
|
+
output: { text: true },
|
|
248
|
+
},
|
|
249
|
+
cost: { input: 1, output: 2, cache: { read: 0.1, write: 0.2 } },
|
|
250
|
+
variants: { low: {}, high: {}, max: {} },
|
|
251
|
+
});
|
|
252
|
+
expect(JSON.stringify(result.value)).not.toContain('sentinel');
|
|
253
|
+
});
|
|
254
|
+
|
|
207
255
|
it('treats empty/null release_date as absent without marking the catalog partial', () => {
|
|
208
256
|
const catalog = safeCatalog();
|
|
209
257
|
catalog.providers[0].models['gpt-5'].release_date = '';
|
|
@@ -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,11 @@ 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';
|
|
23
32
|
|
|
24
33
|
/**
|
|
25
34
|
* v2 protocol mounts every JSON/SSE route under `/api`
|
|
@@ -244,6 +253,8 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
244
253
|
SSE_UPSTREAM_CONNECT_TIMEOUT_MS,
|
|
245
254
|
SSE_UPSTREAM_STALL_TIMEOUT_MS,
|
|
246
255
|
getRuntime,
|
|
256
|
+
/** Optional DI: `{ allowMissingContract: true }` for unit factories only. */
|
|
257
|
+
getRuntimeContractGateOptions = null,
|
|
247
258
|
getOpenCodeAuthHeaders,
|
|
248
259
|
buildOpenCodeUrl,
|
|
249
260
|
ensureOpenCodeApiPrefix,
|
|
@@ -253,50 +264,137 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
253
264
|
// wired, fold its per-session metadata onto list/detail responses so
|
|
254
265
|
// clients keep reading `session.metadata` where they always did.
|
|
255
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,
|
|
256
273
|
} = deps;
|
|
257
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
|
+
|
|
258
295
|
/**
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
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 }}
|
|
262
300
|
*/
|
|
263
301
|
const readStoredSessionMetadata = async () => {
|
|
264
|
-
if (
|
|
302
|
+
if (!storeConfigured) return { status: 'unconfigured' };
|
|
265
303
|
try {
|
|
266
304
|
const stored = await getStoredSessionMetadata();
|
|
267
|
-
|
|
305
|
+
if (!stored || typeof stored !== 'object') {
|
|
306
|
+
return { status: 'unavailable', error: 'session metadata is unavailable' };
|
|
307
|
+
}
|
|
308
|
+
return { status: 'ok', stored };
|
|
268
309
|
} catch (error) {
|
|
269
|
-
|
|
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 {
|
|
270
325
|
return null;
|
|
271
326
|
}
|
|
272
327
|
};
|
|
273
328
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const ours = stored[session.id];
|
|
281
|
-
if (!ours || typeof ours !== 'object' || Array.isArray(ours)) return session;
|
|
282
|
-
const theirs = session.metadata && typeof session.metadata === 'object' && !Array.isArray(session.metadata)
|
|
283
|
-
? session.metadata
|
|
284
|
-
: {};
|
|
285
|
-
return { ...session, metadata: { ...theirs, ...ours } };
|
|
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
|
+
});
|
|
286
335
|
};
|
|
287
336
|
|
|
337
|
+
/**
|
|
338
|
+
* OpenChamber's stored metadata deep-merges onto upstream create-time
|
|
339
|
+
* metadata, and Host archive authority projects onto `time.archived`.
|
|
340
|
+
*/
|
|
288
341
|
const overlaySession = (session, stored) => {
|
|
289
342
|
if (!stored) return session;
|
|
290
|
-
return
|
|
343
|
+
return projectSessionWithStoredMap(session, stored);
|
|
291
344
|
};
|
|
292
345
|
|
|
346
|
+
/**
|
|
347
|
+
* @returns {Promise<{ ok: true, payload: unknown } | { ok: false, error: string }>}
|
|
348
|
+
*/
|
|
293
349
|
const overlayOwnedStateOnList = async (payload) => {
|
|
294
350
|
const records = sessionListRecords(payload);
|
|
295
|
-
if (!records) return payload;
|
|
296
|
-
const
|
|
297
|
-
if (
|
|
298
|
-
|
|
299
|
-
|
|
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
|
+
};
|
|
300
398
|
};
|
|
301
399
|
|
|
302
400
|
if (app.get('opencodeProxyConfigured')) {
|
|
@@ -449,6 +547,15 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
449
547
|
};
|
|
450
548
|
|
|
451
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
|
+
|
|
452
559
|
const abortController = new AbortController();
|
|
453
560
|
const closeUpstream = () => abortController.abort();
|
|
454
561
|
let upstream = null;
|
|
@@ -466,7 +573,13 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
466
573
|
const includeReasoning = readIncludeReasoningQuery(req.query)
|
|
467
574
|
&& readIncludeReasoningFromUrl(requestUrl);
|
|
468
575
|
const reasoningFilter = includeReasoning ? null : createReasoningOutboundFilter();
|
|
469
|
-
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;
|
|
470
583
|
|
|
471
584
|
req.on('close', closeUpstream);
|
|
472
585
|
|
|
@@ -582,20 +695,20 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
582
695
|
}
|
|
583
696
|
if (value && value.length > 0) {
|
|
584
697
|
resetStallTimer();
|
|
585
|
-
if (
|
|
586
|
-
//
|
|
698
|
+
if (!outboundSseFilter || !sseSplitter) {
|
|
699
|
+
// No Host projection: byte-identical passthrough.
|
|
587
700
|
sseBoundary.observe(value);
|
|
588
701
|
const canContinue = await enqueueSseWrite(value);
|
|
589
702
|
if (!canContinue) {
|
|
590
703
|
break;
|
|
591
704
|
}
|
|
592
705
|
} else {
|
|
593
|
-
//
|
|
594
|
-
// 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.
|
|
595
708
|
const blocks = sseSplitter.push(value);
|
|
596
709
|
let canContinue = true;
|
|
597
710
|
for (const block of blocks) {
|
|
598
|
-
const filtered = filterSseBlock(block,
|
|
711
|
+
const filtered = filterSseBlock(block, outboundSseFilter);
|
|
599
712
|
if (!filtered) {
|
|
600
713
|
continue;
|
|
601
714
|
}
|
|
@@ -610,9 +723,9 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
610
723
|
}
|
|
611
724
|
}
|
|
612
725
|
|
|
613
|
-
if (
|
|
726
|
+
if (outboundSseFilter && sseSplitter) {
|
|
614
727
|
for (const block of sseSplitter.finish()) {
|
|
615
|
-
const filtered = filterSseBlock(block,
|
|
728
|
+
const filtered = filterSseBlock(block, outboundSseFilter);
|
|
616
729
|
if (filtered) {
|
|
617
730
|
sseBoundary.observe(filtered);
|
|
618
731
|
await enqueueSseWrite(filtered);
|
|
@@ -641,7 +754,7 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
641
754
|
} finally {
|
|
642
755
|
clearConnectTimer();
|
|
643
756
|
clearStallTimer();
|
|
644
|
-
|
|
757
|
+
outboundSseFilter?.dispose?.();
|
|
645
758
|
if (heartbeatTimer) {
|
|
646
759
|
clearTimeout(heartbeatTimer);
|
|
647
760
|
heartbeatTimer = null;
|
|
@@ -659,7 +772,7 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
659
772
|
}
|
|
660
773
|
};
|
|
661
774
|
|
|
662
|
-
const fetchSessionListPayload = async (upstreamPath, { req = null, timeoutMs = null } = {}) => {
|
|
775
|
+
const fetchSessionListPayload = async (upstreamPath, { req = null, timeoutMs = null, method = 'GET' } = {}) => {
|
|
663
776
|
const headers = req
|
|
664
777
|
? {
|
|
665
778
|
...normalizeForwardedDirectoryHeaders(collectForwardProxyHeaders(req.headers, getOpenCodeAuthHeaders())),
|
|
@@ -672,7 +785,7 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
672
785
|
'accept-encoding': 'identity',
|
|
673
786
|
};
|
|
674
787
|
const upstream = await fetch(buildOpenCodeUrl(upstreamPath, ''), {
|
|
675
|
-
method: 'GET',
|
|
788
|
+
method: method || 'GET',
|
|
676
789
|
headers,
|
|
677
790
|
...(typeof timeoutMs === 'number' ? { signal: AbortSignal.timeout(timeoutMs) } : {}),
|
|
678
791
|
});
|
|
@@ -720,7 +833,11 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
720
833
|
}
|
|
721
834
|
|
|
722
835
|
res.setHeader('content-type', result.contentType);
|
|
723
|
-
|
|
836
|
+
const overlaid = await overlayOwnedStateOnList(sanitizeSessionListPayload(result.payload));
|
|
837
|
+
if (!overlaid.ok) {
|
|
838
|
+
return metadataUnavailableResponse(res, overlaid.error);
|
|
839
|
+
}
|
|
840
|
+
res.json(overlaid.payload);
|
|
724
841
|
} catch (error) {
|
|
725
842
|
if (isAbortError(error)) {
|
|
726
843
|
return;
|
|
@@ -835,12 +952,42 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
835
952
|
req.path.startsWith('/config/settings') ||
|
|
836
953
|
req.path.startsWith('/config/skills') ||
|
|
837
954
|
req.path === '/config/reload' ||
|
|
838
|
-
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'
|
|
839
961
|
) {
|
|
840
962
|
return next();
|
|
841
963
|
}
|
|
842
964
|
|
|
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
|
+
|
|
843
986
|
if (!shouldHoldProxiedRequest(getRuntime())) {
|
|
987
|
+
const lateBlock = blockedByContract();
|
|
988
|
+
if (lateBlock) {
|
|
989
|
+
return res.status(409).json(lateBlock);
|
|
990
|
+
}
|
|
844
991
|
return next();
|
|
845
992
|
}
|
|
846
993
|
|
|
@@ -850,6 +997,11 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
850
997
|
if (res.writableEnded || req.aborted) return;
|
|
851
998
|
const runtimeState = getRuntime();
|
|
852
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
|
+
}
|
|
853
1005
|
return next();
|
|
854
1006
|
}
|
|
855
1007
|
if (!isMigrationBlockingTranscript(runtimeState) && Date.now() >= processDeadline) {
|
|
@@ -938,7 +1090,11 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
938
1090
|
return bTime - aTime;
|
|
939
1091
|
});
|
|
940
1092
|
console.log(`[SessionMerge] ${globalSessions?.length || 0} global + ${extraSessions.length} extra = ${merged.length} total`);
|
|
941
|
-
|
|
1093
|
+
const overlaid = await overlayOwnedStateOnList(sanitizeSessionListPayload(merged));
|
|
1094
|
+
if (!overlaid.ok) {
|
|
1095
|
+
return metadataUnavailableResponse(res, overlaid.error);
|
|
1096
|
+
}
|
|
1097
|
+
return res.json(overlaid.payload);
|
|
942
1098
|
} catch (error) {
|
|
943
1099
|
console.log(`[SessionMerge] Error: ${error.message}`);
|
|
944
1100
|
return res.status(500).json({ error: error.message || 'Failed to merge Windows sessions' });
|
|
@@ -954,7 +1110,7 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
954
1110
|
// with the list it came from. Everything else about the record is forwarded
|
|
955
1111
|
// untouched. Without a store, fall through to the generic proxy.
|
|
956
1112
|
app.get('/api/session/:sessionID', async (req, res, next) => {
|
|
957
|
-
if (
|
|
1113
|
+
if (!storeConfigured) return next();
|
|
958
1114
|
// Nested routes (message, children, …) are registered separately; Express
|
|
959
1115
|
// still matches this pattern for bare session ids only when no further
|
|
960
1116
|
// segment is present — but be defensive if a child path slips through.
|
|
@@ -965,26 +1121,33 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
965
1121
|
const upstreamPath = await getRequestUpstreamPath(req);
|
|
966
1122
|
const result = await fetchSessionListPayload(upstreamPath, { req });
|
|
967
1123
|
|
|
968
|
-
res.status(result.upstream.status);
|
|
969
|
-
applyForwardProxyResponseHeaders(result.upstream.headers, res);
|
|
970
|
-
res.setHeader('content-type', result.contentType);
|
|
971
|
-
|
|
972
1124
|
const record = result.isJson && !result.parseError ? result.payload : null;
|
|
973
1125
|
const session = record && typeof record === 'object' && !Array.isArray(record)
|
|
974
1126
|
? (record.data && typeof record.data === 'object' && !Array.isArray(record.data) ? record.data : record)
|
|
975
1127
|
: null;
|
|
976
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);
|
|
977
1132
|
res.end(result.bodyText);
|
|
978
1133
|
return;
|
|
979
1134
|
}
|
|
980
1135
|
|
|
981
|
-
const
|
|
982
|
-
if (
|
|
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') {
|
|
983
1146
|
res.end(result.bodyText);
|
|
984
1147
|
return;
|
|
985
1148
|
}
|
|
986
1149
|
|
|
987
|
-
const overlaid = overlaySession(session, stored);
|
|
1150
|
+
const overlaid = overlaySession(session, meta.stored);
|
|
988
1151
|
res.json(record.data && typeof record.data === 'object' && !Array.isArray(record.data)
|
|
989
1152
|
? { ...record, data: overlaid }
|
|
990
1153
|
: overlaid);
|
|
@@ -999,11 +1162,54 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
999
1162
|
}
|
|
1000
1163
|
});
|
|
1001
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
|
+
|
|
1002
1205
|
// Official session.messages list (not exact message — that is owned by
|
|
1003
1206
|
// session-turn-pages and registered before this proxy).
|
|
1004
1207
|
app.get('/api/session/:sessionID/message', (req, res, next) => {
|
|
1005
1208
|
return forwardSessionMessagesListRequest(req, res, next);
|
|
1006
1209
|
});
|
|
1210
|
+
app.get('/api/session/:sessionID/context', (req, res, next) => {
|
|
1211
|
+
return forwardSessionMessagesListRequest(req, res, next);
|
|
1212
|
+
});
|
|
1007
1213
|
|
|
1008
1214
|
app.get('/api/global/event', forwardSseRequest);
|
|
1009
1215
|
app.get('/api/event', forwardSseRequest);
|