@openchambery/web 2.0.0-beta.1 → 2.0.0-beta.11
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-pLaqWWmB.js} +1 -1
- package/dist/assets/AssistantView-C4GkABnI.js +3 -0
- package/dist/assets/{DiagramView-xuSJTnv7.js → DiagramView-ByYLIfWo.js} +1 -1
- package/dist/assets/{FadeInOnReveal-Ov0D96d0.js → FadeInOnReveal-MuJAzJq_.js} +1 -1
- package/dist/assets/Icon-Dz2kO9RN.js +1 -0
- package/dist/assets/MarkdownRenderer-CPU6kSHd.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-SKi4oG0c.js → MarkdownRendererImpl-2hw5cdqQ.js} +3 -3
- package/dist/assets/MarkstreamRendererImpl-DXZQkLTa.js +3 -0
- package/dist/assets/{MobileDetailNavigation-Bnx59U_J.js → MobileDetailNavigation-C9BzieQY.js} +1 -1
- package/dist/assets/{MobileShareBridge-BQlP4J-F.js → MobileShareBridge-2_iOfzqO.js} +2 -2
- package/dist/assets/MobileSurface-DNVA7K_1.js +2 -0
- package/dist/assets/{MultiRunWindow-ChZGob0v.js → MultiRunWindow-QA6pMrJ9.js} +1 -1
- package/dist/assets/{OnboardingScreen-DtQJxZG-.js → OnboardingScreen-XWkr7MjT.js} +1 -1
- package/dist/assets/OverlayScrollbar-DCWyGseO.js +2 -0
- package/dist/assets/SettingsView-dWuu0VIb.js +25 -0
- package/dist/assets/{SettingsWindow-DfnsFa8y.js → SettingsWindow-CetgiJMX.js} +1 -1
- package/dist/assets/TerminalView-BuXuo7gm.js +8 -0
- package/dist/assets/{ToolOutputDialog-BejmjrRk.js → ToolOutputDialog-DNSSOaCt.js} +2 -2
- package/dist/assets/appThemeRegistry-Cb7qFBsc.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-Doq9FI1I.js} +1 -1
- package/dist/assets/{collapsible-BXRd5_Hr.js → collapsible-CYUeu3_N.js} +1 -1
- package/dist/assets/{context-menu-jGtH6fR9.js → context-menu-C8kOMVtC.js} +1 -1
- package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-DbxnIQdS.js} +1 -1
- package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-dvxF4V2p.js} +1 -1
- package/dist/assets/{diagram-DQf7Y_uR.js → diagram-B1p9Yn1h.js} +1 -1
- package/dist/assets/{dialog-bfbvR4VH.js → dialog-D08-Lg-e.js} +1 -1
- package/dist/assets/es-Dsmc0A_3.js +15 -0
- package/dist/assets/fileMentionTouchSelection-BCAKb-NS.js +3 -0
- package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-X773xxvX.js} +1 -1
- package/dist/assets/fr-C_ZlZDI2.js +13 -0
- package/dist/assets/gitApi-3VBj3fbR.js +10 -0
- package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-BMztRu0c.js} +1 -1
- package/dist/assets/{input-8r6F_Fza.js → input-DZZuQ1IL.js} +1 -1
- package/dist/assets/insertionBoundaries-BMuCcvPy.js +1 -0
- package/dist/assets/ja-BqqTaWTi.js +15 -0
- package/dist/assets/ko-DP2XPM37.js +15 -0
- package/dist/assets/main-COEILjRF.js +169 -0
- package/dist/assets/main-D2BzpnUt.css +1 -0
- package/dist/assets/{main-CCE_4y5F.js → main-DsUewWPt.js} +2 -2
- package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
- package/dist/assets/{markdownHeightCache-1Qo6y6IJ.js → markdownHeightCache-DBgXGze8.js} +1 -1
- package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
- package/dist/assets/mcpQueries-B05QR67V.js +1 -0
- package/dist/assets/miniChat-D7IPMm-k.js +2 -0
- package/dist/assets/mobile-BnCoDxnv.js +2 -0
- package/dist/assets/multirun-BDmFjX9N.js +4 -0
- package/dist/assets/persistence-DznVIKwz.js +3 -0
- package/dist/assets/pl-BCf5uLlK.js +15 -0
- package/dist/assets/platform-DSTtx-DI.js +6 -0
- package/dist/assets/projectMeta-C5gcAd5f.js +3 -0
- package/dist/assets/pt-BR-W09Q2pvn.js +15 -0
- package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-DsAaWCss.js +2 -0
- package/dist/assets/renderMobileApp-DOaoohlp.js +28 -0
- package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
- package/dist/assets/{runtimeConfig-BMvqa5we.js → runtimeConfig-CzPK1uMj.js} +1 -1
- package/dist/assets/runtimeEndpointReset-DKjVJCdt.js +149 -0
- package/dist/assets/sessionChangeSummary-CktSWasa.js +2 -0
- package/dist/assets/{sessionLookup-gh-hVRIc.js → sessionLookup-CDA1aAcI.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-CudoSgV4.js → sortable-tabs-strip-Q3bb6MVj.js} +1 -1
- package/dist/assets/src-lS1keDVV.css +2 -0
- package/dist/assets/store-jaP79mqI.js +15 -0
- package/dist/assets/{terminalApi-DiF6JmBP.js → terminalApi-Da_DO_Uh.js} +1 -1
- package/dist/assets/textarea--CYehp07.js +1 -0
- package/dist/assets/{tooltip-CQvpK-UV.js → tooltip-CGLzBwEK.js} +1 -1
- package/dist/assets/uk-BgFzz8m2.js +15 -0
- package/dist/assets/{url-_z7evzdK.js → url-Byz3kd29.js} +1 -1
- package/dist/assets/{useAppFontEffects-Cpmpo7Wb.js → useAppFontEffects-2zzPMQF2.js} +64 -45
- package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-Bz4tRflF.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-B-2YooUd.js +1 -0
- package/dist/assets/useEffectiveDirectory-K3U4eL5F.js +1 -0
- package/dist/assets/{useFontPreferences-DcbXQOkc.js → useFontPreferences-D9OIZs1p.js} +1 -1
- package/dist/assets/useMobileNavigationStore-B0zSdCwP.js +414 -0
- package/dist/assets/useSessionAutoCleanup-BCXHAcbX.js +2 -0
- package/dist/assets/useUIStore-syVmbuDA.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-Bqn4AiBL.js → useWorkerHighlightedLines-CsoMQNik.js} +2 -2
- package/dist/assets/vendor-opencode-client-C12gfjag.js +12 -0
- package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-Dq5ocTF8.js} +1 -1
- package/dist/assets/zh-CN-BhqSLgUY.js +15 -0
- package/dist/assets/zh-TW-BsaBgY-p.js +15 -0
- package/dist/index.html +12 -12
- package/dist/mini-chat.html +12 -12
- package/dist/mobile.html +12 -12
- package/package.json +3 -2
- package/server/index.js +359 -63
- 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/browser-provider/DOCUMENTATION.md +141 -0
- package/server/lib/browser-provider/contract.js +312 -0
- package/server/lib/browser-provider/host.js +421 -0
- package/server/lib/browser-provider/host.test.js +304 -0
- package/server/lib/browser-provider/routes.js +122 -0
- package/server/lib/browser-provider/routes.test.js +120 -0
- package/server/lib/browser-provider/selection-store.js +43 -0
- package/server/lib/browser-provider/selection-store.test.js +35 -0
- 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/DOCUMENTATION.md +3 -1
- package/server/lib/notifications/runtime.js +133 -31
- package/server/lib/notifications/runtime.test.js +103 -0
- package/server/lib/notifications/template-runtime.js +130 -38
- package/server/lib/notifications/template-runtime.test.js +96 -0
- package/server/lib/opencode/DOCUMENTATION.md +89 -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.d.ts +8 -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 +53 -4
- package/server/lib/opencode/feature-routes-runtime.test.js +25 -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 +254 -46
- package/server/lib/opencode/proxy.test.js +238 -5
- package/server/lib/opencode/routes.js +326 -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/upgrade-screen.d.ts +30 -0
- package/server/lib/opencode/upgrade-screen.js +299 -0
- package/server/lib/opencode/upgrade-screen.test.js +196 -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 +73 -18
- package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
- package/server/lib/session-metadata/opencode-session-record.js +316 -0
- package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
- package/server/lib/session-metadata/routes.js +112 -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 +38 -0
- package/server/lib/session-metadata/session-metadata-store.js +270 -83
- 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 +279 -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 +69 -44
- 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/Icon-DD-BuMfZ.js +0 -1
- package/dist/assets/MarkdownRenderer-bc_p1H7l.js +0 -2
- 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/TerminalView-Dh9MWCpS.js +0 -8
- package/dist/assets/appThemeRegistry-COGfxTFV.js +0 -1
- package/dist/assets/es-c52LTHHe.js +0 -15
- package/dist/assets/fileMentionTouchSelection-BvrNCroG.js +0 -4
- 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/main-qSeeJRW1.css +0 -1
- package/dist/assets/markdown-worker-CptoICmq.js +0 -2
- package/dist/assets/markdownTheme-DOfGg-A7.js +0 -1
- 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/persistence-Ce3AnpEr.js +0 -3
- 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/vendor-opencode-client-BOe2qpQb.js +0 -12
- 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,44 @@ 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' ||
|
|
961
|
+
req.path === '/opencode/compatibility' ||
|
|
962
|
+
req.path === '/opencode/install-required'
|
|
839
963
|
) {
|
|
840
964
|
return next();
|
|
841
965
|
}
|
|
842
966
|
|
|
967
|
+
// Ticket 11: limit execution when the running serve fails contract admission.
|
|
968
|
+
// Diagnostics, reads, and stop-task stay available; health success alone is not full semantics.
|
|
969
|
+
// Require explicit executionAllowed === true for the *current* instance (null/pending blocks).
|
|
970
|
+
// Gate again after readiness hold so initial-unknown → ready-incompatible cannot slip through.
|
|
971
|
+
const pathForGate = req.originalUrl || req.url || req.path;
|
|
972
|
+
const gateOptions = typeof getRuntimeContractGateOptions === 'function'
|
|
973
|
+
? getRuntimeContractGateOptions()
|
|
974
|
+
: {};
|
|
975
|
+
const blockedByContract = () => {
|
|
976
|
+
const contract = getRuntime()?.runtimeContract;
|
|
977
|
+
if (shouldBlockRuntimeContractExecution(req.method, pathForGate, contract, gateOptions)) {
|
|
978
|
+
return runtimeContractExecutionBlockedBody(contract);
|
|
979
|
+
}
|
|
980
|
+
return null;
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
const earlyBlock = blockedByContract();
|
|
984
|
+
if (earlyBlock) {
|
|
985
|
+
return res.status(409).json(earlyBlock);
|
|
986
|
+
}
|
|
987
|
+
|
|
843
988
|
if (!shouldHoldProxiedRequest(getRuntime())) {
|
|
989
|
+
const lateBlock = blockedByContract();
|
|
990
|
+
if (lateBlock) {
|
|
991
|
+
return res.status(409).json(lateBlock);
|
|
992
|
+
}
|
|
844
993
|
return next();
|
|
845
994
|
}
|
|
846
995
|
|
|
@@ -850,6 +999,11 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
850
999
|
if (res.writableEnded || req.aborted) return;
|
|
851
1000
|
const runtimeState = getRuntime();
|
|
852
1001
|
if (!shouldHoldProxiedRequest(runtimeState)) {
|
|
1002
|
+
// Re-evaluate after wait: startup may have published incompatible/pending.
|
|
1003
|
+
const afterHoldBlock = blockedByContract();
|
|
1004
|
+
if (afterHoldBlock) {
|
|
1005
|
+
return res.status(409).json(afterHoldBlock);
|
|
1006
|
+
}
|
|
853
1007
|
return next();
|
|
854
1008
|
}
|
|
855
1009
|
if (!isMigrationBlockingTranscript(runtimeState) && Date.now() >= processDeadline) {
|
|
@@ -938,7 +1092,11 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
938
1092
|
return bTime - aTime;
|
|
939
1093
|
});
|
|
940
1094
|
console.log(`[SessionMerge] ${globalSessions?.length || 0} global + ${extraSessions.length} extra = ${merged.length} total`);
|
|
941
|
-
|
|
1095
|
+
const overlaid = await overlayOwnedStateOnList(sanitizeSessionListPayload(merged));
|
|
1096
|
+
if (!overlaid.ok) {
|
|
1097
|
+
return metadataUnavailableResponse(res, overlaid.error);
|
|
1098
|
+
}
|
|
1099
|
+
return res.json(overlaid.payload);
|
|
942
1100
|
} catch (error) {
|
|
943
1101
|
console.log(`[SessionMerge] Error: ${error.message}`);
|
|
944
1102
|
return res.status(500).json({ error: error.message || 'Failed to merge Windows sessions' });
|
|
@@ -954,7 +1112,7 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
954
1112
|
// with the list it came from. Everything else about the record is forwarded
|
|
955
1113
|
// untouched. Without a store, fall through to the generic proxy.
|
|
956
1114
|
app.get('/api/session/:sessionID', async (req, res, next) => {
|
|
957
|
-
if (
|
|
1115
|
+
if (!storeConfigured) return next();
|
|
958
1116
|
// Nested routes (message, children, …) are registered separately; Express
|
|
959
1117
|
// still matches this pattern for bare session ids only when no further
|
|
960
1118
|
// segment is present — but be defensive if a child path slips through.
|
|
@@ -965,26 +1123,33 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
965
1123
|
const upstreamPath = await getRequestUpstreamPath(req);
|
|
966
1124
|
const result = await fetchSessionListPayload(upstreamPath, { req });
|
|
967
1125
|
|
|
968
|
-
res.status(result.upstream.status);
|
|
969
|
-
applyForwardProxyResponseHeaders(result.upstream.headers, res);
|
|
970
|
-
res.setHeader('content-type', result.contentType);
|
|
971
|
-
|
|
972
1126
|
const record = result.isJson && !result.parseError ? result.payload : null;
|
|
973
1127
|
const session = record && typeof record === 'object' && !Array.isArray(record)
|
|
974
1128
|
? (record.data && typeof record.data === 'object' && !Array.isArray(record.data) ? record.data : record)
|
|
975
1129
|
: null;
|
|
976
1130
|
if (!session || typeof session.id !== 'string') {
|
|
1131
|
+
res.status(result.upstream.status);
|
|
1132
|
+
applyForwardProxyResponseHeaders(result.upstream.headers, res);
|
|
1133
|
+
res.setHeader('content-type', result.contentType);
|
|
977
1134
|
res.end(result.bodyText);
|
|
978
1135
|
return;
|
|
979
1136
|
}
|
|
980
1137
|
|
|
981
|
-
const
|
|
982
|
-
if (
|
|
1138
|
+
const meta = await readStoredSessionMetadata();
|
|
1139
|
+
if (meta.status === 'unavailable') {
|
|
1140
|
+
return metadataUnavailableResponse(res, meta.error);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
res.status(result.upstream.status);
|
|
1144
|
+
applyForwardProxyResponseHeaders(result.upstream.headers, res);
|
|
1145
|
+
res.setHeader('content-type', result.contentType);
|
|
1146
|
+
|
|
1147
|
+
if (meta.status === 'unconfigured') {
|
|
983
1148
|
res.end(result.bodyText);
|
|
984
1149
|
return;
|
|
985
1150
|
}
|
|
986
1151
|
|
|
987
|
-
const overlaid = overlaySession(session, stored);
|
|
1152
|
+
const overlaid = overlaySession(session, meta.stored);
|
|
988
1153
|
res.json(record.data && typeof record.data === 'object' && !Array.isArray(record.data)
|
|
989
1154
|
? { ...record, data: overlaid }
|
|
990
1155
|
: overlaid);
|
|
@@ -999,11 +1164,54 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
999
1164
|
}
|
|
1000
1165
|
});
|
|
1001
1166
|
|
|
1167
|
+
// Successful upstream delete → idempotent Host metadata cleanup. Failures are
|
|
1168
|
+
// observable (logged) and compensated by session.deleted event ingest.
|
|
1169
|
+
app.delete('/api/session/:sessionID', async (req, res, next) => {
|
|
1170
|
+
if (typeof onSessionDeleted !== 'function') return next();
|
|
1171
|
+
const sessionID = typeof req.params?.sessionID === 'string' ? req.params.sessionID.trim() : '';
|
|
1172
|
+
if (!sessionID || sessionID.includes('/')) return next();
|
|
1173
|
+
try {
|
|
1174
|
+
const upstreamPath = await getRequestUpstreamPath(req);
|
|
1175
|
+
const result = await fetchSessionListPayload(upstreamPath, {
|
|
1176
|
+
req,
|
|
1177
|
+
method: 'DELETE',
|
|
1178
|
+
});
|
|
1179
|
+
res.status(result.upstream.status);
|
|
1180
|
+
applyForwardProxyResponseHeaders(result.upstream.headers, res);
|
|
1181
|
+
res.setHeader('content-type', result.contentType || 'application/json');
|
|
1182
|
+
if (result.upstream.status >= 200 && result.upstream.status < 300) {
|
|
1183
|
+
try {
|
|
1184
|
+
const cleanup = onSessionDeleted(sessionID);
|
|
1185
|
+
if (cleanup && typeof cleanup.catch === 'function') {
|
|
1186
|
+
cleanup.catch((error) => {
|
|
1187
|
+
console.warn('[proxy] session metadata cleanup after delete failed (retry via event):', error?.message ?? error);
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
console.warn('[proxy] session metadata cleanup after delete failed (retry via event):', error?.message ?? error);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
// Upstream failure keeps Host metadata (do not cleanup).
|
|
1195
|
+
res.end(result.bodyText);
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
if (isAbortError(error)) return;
|
|
1198
|
+
console.error('[proxy] OpenCode session.delete proxy error:', error?.message ?? error);
|
|
1199
|
+
if (!res.headersSent) {
|
|
1200
|
+
res.status(503).json({ error: 'OpenCode service unavailable' });
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
next(error);
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1002
1207
|
// Official session.messages list (not exact message — that is owned by
|
|
1003
1208
|
// session-turn-pages and registered before this proxy).
|
|
1004
1209
|
app.get('/api/session/:sessionID/message', (req, res, next) => {
|
|
1005
1210
|
return forwardSessionMessagesListRequest(req, res, next);
|
|
1006
1211
|
});
|
|
1212
|
+
app.get('/api/session/:sessionID/context', (req, res, next) => {
|
|
1213
|
+
return forwardSessionMessagesListRequest(req, res, next);
|
|
1214
|
+
});
|
|
1007
1215
|
|
|
1008
1216
|
app.get('/api/global/event', forwardSseRequest);
|
|
1009
1217
|
app.get('/api/event', forwardSseRequest);
|