@vantran-se/openchamber-web 1.24.4 → 2.0.2
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 +2 -8
- package/bin/lib/commands-connect-url.js +3 -8
- package/bin/lib/commands-tunnel.js +3 -0
- package/dist/assets/{CodeMirrorCommentWidgets-B4a0JkUz.js → CodeMirrorCommentWidgets-B82Oye3V.js} +1 -1
- package/dist/assets/DiffView-DJLmWn6Z.js +4 -0
- package/dist/assets/FilesView-DjksmZRo.js +17 -0
- package/dist/assets/{GitView-Byu2DY7s.js → GitView-DiUsUIgr.js} +4 -4
- package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-CYwWYLr3.js} +1 -1
- package/dist/assets/GuestSurfacePane-CmctyRI4.js +1 -0
- package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-DOHHcbgJ.js} +1 -1
- package/dist/assets/LinearIssuesView-DaqnylcV.js +1 -0
- package/dist/assets/MarkdownImageGallery-HJNvWt7L.js +1 -0
- package/dist/assets/MarkdownRendererImpl-nDZRGg2I.js +12 -0
- package/dist/assets/OnboardingScreen-C2B5Bsof.js +1 -0
- package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-C4FLBjpt.js} +1 -1
- package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-C_1S9c9n.js} +1 -1
- package/dist/assets/PlanView-_eZvb-G5.js +5 -0
- package/dist/assets/PluginPane-sM8fcZIV.js +3 -0
- package/dist/assets/SettingsView-BRfr46hb.js +29 -0
- package/dist/assets/SettingsWindow-CcUDwQHg.js +1 -0
- package/dist/assets/SubmoduleDiffSummary-CrN1ZGwl.js +1 -0
- package/dist/assets/ToolOutputDialog-Cij4jI09.js +15 -0
- package/dist/assets/ToolPartDiffPreview-BXzwQ-JY.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-DSWTLJNH.js} +4 -4
- package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-CpUUgY8H.js} +2 -2
- package/dist/assets/appThemeRegistry-B5tHTkR8.js +1 -0
- package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-D7D8qV8p.js} +1 -1
- package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-cq9uQ30i.js} +1 -1
- package/dist/assets/de-CUO1-KqF.js +10 -0
- package/dist/assets/es-BlbC6sGJ.js +16 -0
- package/dist/assets/fr-D2nwe_0W.js +14 -0
- package/dist/assets/index-BQOoYIOu.js +1 -0
- package/dist/assets/index-CLcM8HKx.css +1 -0
- package/dist/assets/index-CvLgnuNI.js +41 -0
- package/dist/assets/{isDiffPlainText-CO4e5J_S.js → isDiffPlainText-D0obJ2p8.js} +1 -1
- package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-DB3_rjyW.js} +1 -1
- package/dist/assets/ja-CA7vnDsZ.js +16 -0
- package/dist/assets/ko-Chy6a9l-.js +16 -0
- package/dist/assets/main-B9JFK3vF.js +896 -0
- package/dist/assets/main-Bhiqnrhd.js +2 -0
- package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-tfqOFaBd.js} +4 -4
- package/dist/assets/miniChat-B0UOL5zA.js +2 -0
- package/dist/assets/mobile-BJppFIEo.js +2 -0
- package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-Db6KuwCP.js} +3 -3
- package/dist/assets/pl-CTR5A_1P.js +16 -0
- package/dist/assets/pt-BR-BE1O1KEx.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-Yz5sfoFh.js +2 -0
- package/dist/assets/renderMobileApp-QQ-yB8aY.js +25 -0
- package/dist/assets/runtimeEndpointReset-DKDdzRzm.js +27 -0
- package/dist/assets/scheduledTasksApi-C5sCuNqf.js +1 -0
- package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-xURoo5t_.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-BZLGuYTU.js} +1 -1
- package/dist/assets/tr-Bh5vK6cW.js +7 -0
- package/dist/assets/uk-CLDkZFy_.js +16 -0
- package/dist/assets/useAppFontEffects-BxrtCnfR.js +7337 -0
- package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CeXuR-SR.js} +2 -2
- package/dist/assets/useRootScrollLock-DgWxezc1.js +2 -0
- package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-DOuaQUZ0.js} +2 -2
- package/dist/assets/vendor-cron-parser-CjvYVigk.js +2 -0
- package/dist/assets/vendor-opencode-client-_BAHQo4R.js +12 -0
- package/dist/assets/{vendor-remixicon-react-BDmGmFCA.js → vendor-remixicon-react-D6dZ0UwF.js} +1 -1
- package/dist/assets/{vendor-zod-CgAdXWZO.js → vendor-zod-Gea9uBlY.js} +1 -1
- package/dist/assets/{web-components-DxX8bZ1o.js → web-components-63H1h21j.js} +2 -2
- package/dist/assets/zh-CN-DODgcmYj.js +16 -0
- package/dist/assets/zh-TW-ByNfIwLA.js +16 -0
- package/dist/index.html +13 -7
- package/dist/mini-chat.html +5 -5
- package/dist/mobile.html +5 -5
- package/package.json +6 -5
- package/server/index.d.ts +1 -0
- package/server/index.js +246 -91
- package/server/lib/agent-tool/DOCUMENTATION.md +32 -15
- package/server/lib/agent-tool/runtime.js +135 -54
- package/server/lib/agent-tool/runtime.test.js +155 -87
- package/server/lib/browser-control/DOCUMENTATION.md +37 -2
- package/server/lib/browser-control/provider.js +217 -0
- package/server/lib/browser-control/provider.test.js +251 -0
- package/server/lib/context-obligatory/runtime.js +61 -50
- package/server/lib/context-obligatory/runtime.test.js +62 -162
- package/server/lib/event-stream/DOCUMENTATION.md +12 -1
- package/server/lib/event-stream/delta-coalescer.js +0 -0
- package/server/lib/event-stream/delta-coalescer.test.js +68 -57
- package/server/lib/event-stream/directory-ws-bridge.js +1 -1
- package/server/lib/event-stream/global-hub.js +10 -1
- package/server/lib/event-stream/global-hub.test.js +27 -26
- package/server/lib/event-stream/index.js +6 -0
- package/server/lib/event-stream/protocol.js +26 -19
- package/server/lib/event-stream/protocol.test.js +18 -7
- package/server/lib/event-stream/rebind.test.js +5 -5
- package/server/lib/event-stream/runtime.test.js +17 -15
- package/server/lib/event-stream/translate-v2.js +313 -0
- package/server/lib/event-stream/translate-v2.test.js +209 -0
- package/server/lib/event-stream/upstream-reader.test.js +16 -16
- package/server/lib/fs/DOCUMENTATION.md +4 -2
- package/server/lib/fs/byte-range.js +35 -0
- package/server/lib/fs/byte-range.test.js +31 -0
- package/server/lib/fs/routes.js +47 -42
- package/server/lib/fs/routes.test.js +123 -18
- package/server/lib/git/service.js +12 -0
- package/server/lib/github/DOCUMENTATION.md +1 -1
- package/server/lib/github/pr-status.js +22 -2
- package/server/lib/github/pr-status.test.js +27 -14
- package/server/lib/guests/DOCUMENTATION.md +13 -0
- package/server/lib/guests/catalog.js +12 -0
- package/server/lib/guests/catalog.test.js +33 -0
- package/server/lib/guests/routes.js +10 -1
- package/server/lib/guests/service.js +192 -14
- package/server/lib/guests/service.test.js +201 -1
- package/server/lib/guests/surface.js +630 -0
- package/server/lib/guests/surface.test.js +486 -0
- package/server/lib/markdown-image-grants/routes.js +18 -5
- package/server/lib/markdown-image-grants/routes.test.js +9 -6
- package/server/lib/message-queue/DOCUMENTATION.md +33 -22
- package/server/lib/message-queue/runtime.js +155 -100
- package/server/lib/message-queue/runtime.test.js +168 -82
- package/server/lib/notifications/DOCUMENTATION.md +28 -0
- package/server/lib/notifications/control-stream.http.test.js +68 -0
- package/server/lib/notifications/emitter-runtime.js +15 -0
- package/server/lib/notifications/emitter-runtime.test.js +32 -0
- package/server/lib/notifications/push-runtime.js +5 -1
- package/server/lib/notifications/push-runtime.test.js +2 -2
- package/server/lib/notifications/routes.js +1 -1
- package/server/lib/notifications/runtime.js +115 -26
- package/server/lib/notifications/template-runtime.js +13 -15
- package/server/lib/notifications/template-runtime.test.js +16 -9
- package/server/lib/notifications/trigger-runtime.test.js +187 -0
- package/server/lib/openchamber-control/DOCUMENTATION.md +10 -0
- package/server/lib/openchamber-control/actions.js +3 -2
- package/server/lib/openchamber-control/error.js +1 -1
- package/server/lib/openchamber-control/file-open.js +59 -0
- package/server/lib/openchamber-control/file-open.test.js +57 -0
- package/server/lib/openchamber-control/routes.test.js +12 -0
- package/server/lib/openchamber-control/service.js +102 -49
- package/server/lib/openchamber-control/service.test.js +84 -44
- package/server/lib/openchamber-sessions/archive-store.js +224 -0
- package/server/lib/openchamber-sessions/archive-store.test.js +187 -0
- package/server/lib/openchamber-sessions/opencode-client.js +22 -0
- package/server/lib/openchamber-sessions/routes.js +398 -328
- package/server/lib/openchamber-sessions/routes.test.js +777 -740
- package/server/lib/openchamber-sessions/session-metadata-store.js +285 -0
- package/server/lib/openchamber-sessions/session-metadata-store.test.js +259 -0
- package/server/lib/opencode/DOCUMENTATION.md +449 -29
- package/server/lib/opencode/agents.js +233 -327
- package/server/lib/opencode/auth.js +41 -44
- package/server/lib/opencode/auth.test.js +92 -0
- package/server/lib/opencode/cli-upgrade.d.ts +4 -0
- package/server/lib/opencode/cli-upgrade.js +18 -0
- package/server/lib/opencode/cli-upgrade.test.js +36 -0
- package/server/lib/opencode/commands.js +164 -145
- package/server/lib/opencode/compatibility.d.ts +16 -0
- package/server/lib/opencode/compatibility.js +88 -0
- package/server/lib/opencode/compatibility.test.js +62 -0
- package/server/lib/opencode/config-entity-routes.js +58 -29
- package/server/lib/opencode/config-entity-v2.test.js +290 -0
- package/server/lib/opencode/config-mutation-response.js +17 -7
- package/server/lib/opencode/config-mutation-response.test.js +4 -7
- package/server/lib/opencode/config-paths.test.js +2 -2
- package/server/lib/opencode/config-v2.d.ts +183 -0
- package/server/lib/opencode/config-v2.js +801 -0
- package/server/lib/opencode/config-v2.test.js +466 -0
- package/server/lib/opencode/credential-db.js +144 -0
- package/server/lib/opencode/credential-db.test.js +104 -0
- package/server/lib/opencode/env-runtime.js +48 -9
- package/server/lib/opencode/env-runtime.test.js +45 -0
- package/server/lib/opencode/feature-routes-runtime.js +15 -5
- package/server/lib/opencode/lifecycle-process.test.js +7 -4
- package/server/lib/opencode/lifecycle.js +85 -30
- package/server/lib/opencode/lifecycle.test.js +80 -8
- package/server/lib/opencode/login-shell-env.js +27 -0
- package/server/lib/opencode/managed-config-file.js +119 -0
- package/server/lib/opencode/managed-config-file.test.js +152 -0
- package/server/lib/opencode/managed-plugin-config.js +33 -13
- package/server/lib/opencode/mcp.js +43 -149
- package/server/lib/opencode/network-runtime.js +6 -7
- package/server/lib/opencode/openchamber-routes.test.js +2 -5
- package/server/lib/opencode/plugin-routes.js +3 -3
- package/server/lib/opencode/plugin-routes.test.js +6 -24
- package/server/lib/opencode/plugins.js +91 -40
- package/server/lib/opencode/plugins.test.js +28 -15
- package/server/lib/opencode/providers.js +63 -83
- package/server/lib/opencode/providers.test.js +128 -59
- package/server/lib/opencode/proxy-agent-wiring.test.js +4 -2
- package/server/lib/opencode/proxy.js +179 -129
- package/server/lib/opencode/pwa-manifest-routes.js +4 -2
- package/server/lib/opencode/pwa-manifest-routes.test.js +3 -2
- package/server/lib/opencode/response-envelope.js +14 -0
- package/server/lib/opencode/response-envelope.test.js +23 -0
- package/server/lib/opencode/routes-upgrade.test.js +73 -116
- package/server/lib/opencode/routes.js +121 -428
- package/server/lib/opencode/server-utils-runtime.js +10 -4
- package/server/lib/opencode/session-activity.js +78 -0
- package/server/lib/opencode/session-runtime.js +21 -10
- package/server/lib/opencode/session-runtime.test.js +9 -9
- package/server/lib/opencode/settings-helpers.js +9 -9
- package/server/lib/opencode/settings-helpers.test.js +7 -12
- package/server/lib/opencode/settings-registry.json +6 -8
- package/server/lib/opencode/settings-runtime.js +18 -1
- package/server/lib/opencode/settings-runtime.test.js +31 -2
- package/server/lib/opencode/shared.js +54 -22
- package/server/lib/opencode/shared.test.js +8 -4
- package/server/lib/opencode/shutdown-runtime.js +54 -7
- package/server/lib/opencode/shutdown-runtime.test.js +224 -30
- package/server/lib/opencode/skill-routes.js +23 -24
- package/server/lib/opencode/skills.js +7 -5
- package/server/lib/opencode/tunnel-wiring-runtime.js +2 -1
- package/server/lib/opencode/upgrade-capability.test.js +1 -1
- package/server/lib/opencode/v1-migration-topup.js +288 -0
- package/server/lib/opencode/v1-migration-topup.test.js +266 -0
- package/server/lib/opencode/v2-install.d.ts +8 -0
- package/server/lib/opencode/v2-install.js +158 -0
- package/server/lib/opencode/v2-install.test.js +124 -0
- package/server/lib/opencode/watcher.js +10 -3
- package/server/lib/opencode/watcher.test.js +17 -21
- package/server/lib/opencode/websearch-config.js +66 -0
- package/server/lib/package-manager.js +55 -68
- package/server/lib/package-manager.test.js +241 -75
- package/server/lib/permission-auto-accept/runtime.js +38 -22
- package/server/lib/permission-auto-accept/runtime.test.js +39 -17
- package/server/lib/quota/DOCUMENTATION.md +4 -0
- package/server/lib/quota/providers/xai.js +4 -4
- package/server/lib/relay/tunnel-host.js +4 -1
- package/server/lib/relay/tunnel-host.test.js +7 -0
- package/server/lib/routing/DOCUMENTATION.md +52 -21
- package/server/lib/routing/defaults.js +23 -6
- package/server/lib/routing/jev.js +17 -7
- package/server/lib/routing/routes.http.test.js +68 -33
- package/server/lib/routing/routes.js +65 -22
- package/server/lib/routing/runtime.js +145 -48
- package/server/lib/routing/runtime.test.js +79 -55
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -0
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +51 -46
- package/server/lib/scheduled-tasks/runtime.js +81 -74
- package/server/lib/scheduled-tasks/runtime.test.js +53 -1
- package/server/lib/session-assist/DOCUMENTATION.md +8 -3
- package/server/lib/session-assist/context.js +122 -46
- package/server/lib/session-assist/context.test.js +117 -49
- package/server/lib/session-assist/runtime.js +52 -22
- package/server/lib/session-assist/runtime.test.js +145 -173
- package/server/lib/session-goal/DOCUMENTATION.md +23 -15
- package/server/lib/session-goal/create.js +9 -12
- package/server/lib/session-goal/create.test.js +27 -28
- package/server/lib/session-goal/runtime.js +164 -77
- package/server/lib/session-goal/runtime.test.js +271 -519
- package/server/lib/session-knowledge/DOCUMENTATION.md +5 -0
- package/server/lib/session-knowledge/runtime.js +59 -41
- package/server/lib/session-knowledge/runtime.test.js +90 -20
- package/server/lib/small-model/DOCUMENTATION.md +154 -208
- package/server/lib/small-model/client.js +113 -0
- package/server/lib/small-model/index.js +296 -158
- package/server/lib/small-model/index.test.js +423 -256
- package/server/lib/small-model/routes.js +1 -1
- package/server/lib/spaces/DOCUMENTATION.md +807 -0
- package/server/lib/spaces/code-in-bait.js +185 -0
- package/server/lib/spaces/code-in.js +641 -0
- package/server/lib/spaces/code-in.test.js +1048 -0
- package/server/lib/spaces/errors.js +8 -0
- package/server/lib/spaces/exec-http.js +94 -0
- package/server/lib/spaces/gatekeeper-channel.js +202 -0
- package/server/lib/spaces/gatekeeper-channel.test.js +215 -0
- package/server/lib/spaces/gatekeeper-program.cjs +914 -0
- package/server/lib/spaces/gatekeeper-program.test.js +1391 -0
- package/server/lib/spaces/hardening.js +457 -0
- package/server/lib/spaces/hardening.test.js +368 -0
- package/server/lib/spaces/host-git.js +127 -0
- package/server/lib/spaces/host-git.test.js +137 -0
- package/server/lib/spaces/labels.js +234 -0
- package/server/lib/spaces/labels.test.js +219 -0
- package/server/lib/spaces/layout.js +184 -0
- package/server/lib/spaces/layout.test.js +30 -0
- package/server/lib/spaces/manager.js +82 -0
- package/server/lib/spaces/manager.test.js +114 -0
- package/server/lib/spaces/places/code-in.docker.live.test.js +216 -0
- package/server/lib/spaces/places/contract-suite.js +156 -0
- package/server/lib/spaces/places/contract.docker.live.test.js +7 -0
- package/server/lib/spaces/places/contract.test.js +6 -0
- package/server/lib/spaces/places/docker-engine.js +126 -0
- package/server/lib/spaces/places/docker-live-support.js +202 -0
- package/server/lib/spaces/places/docker-tools.js +186 -0
- package/server/lib/spaces/places/docker.js +587 -0
- package/server/lib/spaces/places/docker.test.js +1610 -0
- package/server/lib/spaces/places/escape-suite.js +1018 -0
- package/server/lib/spaces/places/escape.docker.live.test.js +7 -0
- package/server/lib/spaces/places/fake-docker.js +375 -0
- package/server/lib/spaces/places/memory-place.js +103 -0
- package/server/lib/spaces/places/packed.docker.live.test.js +77 -0
- package/server/lib/spaces/places/registry.js +43 -0
- package/server/lib/spaces/places/registry.test.js +50 -0
- package/server/lib/spaces/places/server.docker.live.test.js +225 -0
- package/server/lib/spaces/run-command.js +261 -0
- package/server/lib/spaces/run-command.test.js +396 -0
- package/server/lib/spaces/space-server.js +190 -0
- package/server/lib/spaces/space-server.test.js +335 -0
- package/server/lib/spaces/tools-filler.js +82 -0
- package/server/lib/spaces/tools-filler.test.js +148 -0
- package/server/lib/spaces/tools-pack.js +49 -0
- package/server/lib/spaces/tools-pack.test.js +57 -0
- package/server/lib/spaces/tools.js +138 -0
- package/server/lib/spaces/tools.test.js +153 -0
- package/server/lib/tts/openai-sdk.js +11 -0
- package/server/lib/tts/service.js +7 -5
- package/server/lib/tts/stt.js +3 -2
- package/server/lib/tunnels/DOCUMENTATION.md +2 -0
- package/server/lib/tunnels/routes.js +7 -0
- package/server/lib/tunnels/routes.test.js +55 -0
- package/server/lib/ui-auth/ui-auth.js +38 -2
- package/server/lib/ui-auth/ui-auth.test.js +42 -0
- package/server/lib/ui-auth/ui-passkeys.js +12 -10
- package/server/lib/walkthrough/DOCUMENTATION.md +10 -1
- package/server/lib/walkthrough/pull-request.js +61 -8
- package/server/lib/walkthrough/pull-request.test.js +54 -1
- package/server/lib/walkthrough/routes.js +20 -0
- package/server/lib/walkthrough/routes.test.js +19 -0
- package/server/opencode-proxy.test.js +190 -166
- package/server/opencode-worktree-readiness.test.js +5 -4
- package/dist/assets/DiffView-B2CD9wC8.js +0 -4
- package/dist/assets/FilesView-C4vMFclc.js +0 -13
- package/dist/assets/LinearIssuesView-EzGNCGrd.js +0 -1
- package/dist/assets/MarkdownImageGallery-B2YUDFaB.js +0 -1
- package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +0 -12
- package/dist/assets/OnboardingScreen-CQHBSTEi.js +0 -1
- package/dist/assets/PlanView-D3x-pTkZ.js +0 -5
- package/dist/assets/PluginPane-DEcgDQ9h.js +0 -3
- package/dist/assets/SettingsView-CSinuH-1.js +0 -29
- package/dist/assets/SettingsWindow-B5CHoJFZ.js +0 -1
- package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +0 -1
- package/dist/assets/ToolOutputDialog-tLNWmfcW.js +0 -15
- package/dist/assets/ToolPartDiffPreview-CflzmK-x.js +0 -8
- package/dist/assets/appThemeRegistry-DLicw4dH.js +0 -1
- package/dist/assets/de-Bro6_zyl.js +0 -10
- package/dist/assets/es-Bb9XQZvd.js +0 -16
- package/dist/assets/fr-Dgfzn-IL.js +0 -14
- package/dist/assets/index-CtQNmqUL.js +0 -1
- package/dist/assets/index-DqaMWNrC.css +0 -1
- package/dist/assets/index-Dqc6USl3.js +0 -40
- package/dist/assets/ja-BYsakGFh.js +0 -16
- package/dist/assets/ko-BSaQz9aN.js +0 -16
- package/dist/assets/main-B62_pjL5.js +0 -2
- package/dist/assets/main-BEdcOI2P.js +0 -896
- package/dist/assets/miniChat-LT8Rqofr.js +0 -2
- package/dist/assets/mobile-CqarEynh.js +0 -2
- package/dist/assets/pl-OIviPdoj.js +0 -16
- package/dist/assets/pt-BR-Cltg-E6s.js +0 -16
- package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +0 -2
- package/dist/assets/renderMobileApp-BC1MHg6g.js +0 -25
- package/dist/assets/runtimeEndpointReset-DdrazOVH.js +0 -25
- package/dist/assets/scheduledTasksApi-B2SWMGEU.js +0 -1
- package/dist/assets/tr-CQI-KPre.js +0 -7
- package/dist/assets/uk-CqW7R4DA.js +0 -16
- package/dist/assets/useAppFontEffects-ne84JBVL.js +0 -7333
- package/dist/assets/useRootScrollLock-Cpf1qoGp.js +0 -2
- package/dist/assets/vendor-cron-parser-CUcP7OfX.js +0 -2
- package/dist/assets/vendor-opencode-sdk-BJ73u-bH.js +0 -7
- package/dist/assets/zh-CN-CVZG_o_J.js +0 -16
- package/dist/assets/zh-TW-D5ZxzCZp.js +0 -16
- package/server/lib/opencode/mcp-oauth-callback.test.js +0 -111
- package/server/lib/routing/feature-flag.js +0 -11
- package/server/lib/small-model/call.js +0 -851
- package/server/lib/small-model/call.test.js +0 -1292
- package/server/lib/small-model/catalog.js +0 -13
- package/server/lib/small-model/resolve.js +0 -131
- package/server/lib/small-model/resolve.test.js +0 -197
- package/server/lib/small-model/runtime-providers.js +0 -164
- package/server/lib/small-model/runtime-providers.test.js +0 -118
- package/server/lib/system-prompt/DOCUMENTATION.md +0 -36
- package/server/lib/system-prompt/runtime.js +0 -49
- package/server/lib/system-prompt/runtime.test.js +0 -76
- package/server/lib/walkthrough/reproduce-2607.test.js +0 -148
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import request from 'supertest';
|
|
3
|
-
import {
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
4
|
|
|
5
5
|
import { registerRoutingPromptRewrite, registerRoutingRoutes } from './routes.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* The
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* The send rewrite sits in front of the generic OpenCode proxy, which replays
|
|
9
|
+
* `req.body` when a middleware parsed it. These tests mount the rewrite ahead
|
|
10
|
+
* of a stand-in proxy that records what it would forward.
|
|
11
11
|
*/
|
|
12
|
-
const createApp = ({
|
|
13
|
-
process.env.OPENCHAMBER_ROUTING_ENABLE = flag;
|
|
12
|
+
const createApp = ({ routeSend, autoSessions = new Set() } = {}) => {
|
|
14
13
|
const forwarded = [];
|
|
15
14
|
const runtime = {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
noteModelSelection: vi.fn((sessionId, model) => {
|
|
16
|
+
const auto = model?.providerID === 'openchamber' && model?.id === 'auto';
|
|
17
|
+
if (auto) autoSessions.add(sessionId);
|
|
18
|
+
else autoSessions.delete(sessionId);
|
|
19
|
+
return auto;
|
|
19
20
|
}),
|
|
20
|
-
|
|
21
|
+
isAutoSession: (sessionId) => autoSessions.has(sessionId),
|
|
22
|
+
routeSend: routeSend ?? vi.fn(async ({ body }) => {
|
|
23
|
+
if (typeof body?.command === 'string') body.model = { providerID: 'openai', id: 'gpt-6-astra' };
|
|
24
|
+
return {};
|
|
25
|
+
}),
|
|
26
|
+
describe: async () => ({ available: true, autoReady: true, tokenPresent: true, jevSource: 'typesafe', config: null, builtins: [] }),
|
|
21
27
|
heldPermissions: () => [],
|
|
22
28
|
updateConfig: vi.fn(async () => ({ available: true })),
|
|
23
29
|
setToken: vi.fn(async () => ({ available: true, tokenPresent: true })),
|
|
@@ -43,37 +49,70 @@ const createApp = ({ flag = '1', resolvePromptBody } = {}) => {
|
|
|
43
49
|
return { app, runtime, forwarded };
|
|
44
50
|
};
|
|
45
51
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe('routing prompt rewrite', () => {
|
|
51
|
-
it('rewrites the Auto sentinel on prompt_async and passes the directory along', async () => {
|
|
52
|
+
describe('routing send rewrite', () => {
|
|
53
|
+
it('swallows the Auto sentinel on a model switch instead of forwarding it', async () => {
|
|
52
54
|
const { app, runtime, forwarded } = createApp();
|
|
53
55
|
await request(app)
|
|
54
|
-
.post('/api/session/s1/
|
|
55
|
-
.send({ model: { providerID: 'openchamber',
|
|
56
|
+
.post('/api/session/s1/model?directory=%2Frepo')
|
|
57
|
+
.send({ model: { providerID: 'openchamber', id: 'auto' } })
|
|
58
|
+
.expect(204);
|
|
59
|
+
expect(forwarded).toEqual([]);
|
|
60
|
+
expect(runtime.noteModelSelection).toHaveBeenCalledWith('s1', { providerID: 'openchamber', id: 'auto' }, '/repo');
|
|
61
|
+
expect(runtime.isAutoSession('s1')).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('drops the Auto sentinel from a session create so OpenCode never stores it', async () => {
|
|
65
|
+
const { app, forwarded, runtime } = createApp();
|
|
66
|
+
await request(app)
|
|
67
|
+
.post('/api/session?directory=%2Frepo')
|
|
68
|
+
.send({ title: 'btw', model: { providerID: 'openchamber', id: 'auto' }, agent: 'build' })
|
|
56
69
|
.expect(204);
|
|
57
|
-
expect(forwarded).toEqual([{ path: '/session
|
|
58
|
-
expect(runtime.
|
|
70
|
+
expect(forwarded).toEqual([{ path: '/session', parsed: true, body: { title: 'btw', agent: 'build' } }]);
|
|
71
|
+
expect(runtime.noteModelSelection).not.toHaveBeenCalled();
|
|
72
|
+
|
|
73
|
+
await request(app).post('/api/session').send({ model: { providerID: 'anthropic', id: 'claude-opus-5' } }).expect(204);
|
|
74
|
+
expect(forwarded[1]).toEqual({ path: '/session', parsed: true, body: { model: { providerID: 'anthropic', id: 'claude-opus-5' } } });
|
|
59
75
|
});
|
|
60
76
|
|
|
61
|
-
it('
|
|
62
|
-
const
|
|
63
|
-
await request(
|
|
64
|
-
|
|
65
|
-
expect(
|
|
77
|
+
it('forwards a real model switch untouched and takes the session off Auto', async () => {
|
|
78
|
+
const { app, forwarded, runtime } = createApp();
|
|
79
|
+
await request(app).post('/api/session/s1/model').send({ model: { providerID: 'openchamber', id: 'auto' } }).expect(204);
|
|
80
|
+
await request(app).post('/api/session/s1/model').send({ model: { providerID: 'anthropic', id: 'claude-opus-5' } }).expect(204);
|
|
81
|
+
expect(forwarded).toEqual([{ path: '/session/s1/model', parsed: true, body: { model: { providerID: 'anthropic', id: 'claude-opus-5' } } }]);
|
|
82
|
+
expect(runtime.isAutoSession('s1')).toBe(false);
|
|
83
|
+
});
|
|
66
84
|
|
|
67
|
-
|
|
85
|
+
it('routes a prompt in an Auto session and passes the directory along', async () => {
|
|
86
|
+
const autoSessions = new Set(['s1']);
|
|
87
|
+
const { app, runtime, forwarded } = createApp({ autoSessions });
|
|
88
|
+
await request(app)
|
|
89
|
+
.post('/api/session/s1/prompt?directory=%2Frepo')
|
|
90
|
+
.send({ text: 'hi' })
|
|
91
|
+
.expect(204);
|
|
92
|
+
expect(forwarded).toEqual([{ path: '/session/s1/prompt', parsed: true, body: { text: 'hi' } }]);
|
|
93
|
+
expect(runtime.routeSend).toHaveBeenCalledWith({ sessionId: 's1', directory: '/repo', body: { text: 'hi' } });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('leaves a send in a session that is not on Auto unread', async () => {
|
|
97
|
+
const { app, runtime, forwarded } = createApp();
|
|
98
|
+
await request(app).post('/api/session/s1/prompt').send({ text: 'hi' }).expect(204);
|
|
99
|
+
expect(runtime.routeSend).not.toHaveBeenCalled();
|
|
100
|
+
expect(forwarded[0].parsed).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('leaves the stream untouched when the body is not JSON', async () => {
|
|
104
|
+
const text = createApp({ autoSessions: new Set(['s1']) });
|
|
68
105
|
await request(text.app).post('/api/session/s1/command').set('content-type', 'text/plain').send('raw').expect(204);
|
|
69
106
|
expect(text.forwarded[0]).toEqual({ path: '/session/s1/command', parsed: false, raw: 'raw' });
|
|
70
107
|
});
|
|
71
108
|
|
|
72
|
-
|
|
109
|
+
|
|
110
|
+
it('answers with the runtime error instead of forwarding an unroutable send', async () => {
|
|
73
111
|
const { app, forwarded } = createApp({
|
|
74
|
-
|
|
112
|
+
autoSessions: new Set(['s1']),
|
|
113
|
+
routeSend: vi.fn(async () => { throw Object.assign(new Error('no fallback'), { status: 400 }); }),
|
|
75
114
|
});
|
|
76
|
-
const response = await request(app).post('/api/session/s1/
|
|
115
|
+
const response = await request(app).post('/api/session/s1/prompt').send({ text: 'hi' });
|
|
77
116
|
expect(response.status).toBe(400);
|
|
78
117
|
expect(response.body).toEqual({ error: 'no fallback' });
|
|
79
118
|
expect(forwarded).toEqual([]);
|
|
@@ -93,8 +132,4 @@ describe('routing routes', () => {
|
|
|
93
132
|
expect(runtime.clearToken).toHaveBeenCalled();
|
|
94
133
|
});
|
|
95
134
|
|
|
96
|
-
it('is absent without the feature flag', async () => {
|
|
97
|
-
const { app } = createApp({ flag: '' });
|
|
98
|
-
await request(app).put('/api/routing/token').send({ token: 'x' }).expect(404);
|
|
99
|
-
});
|
|
100
135
|
});
|
|
@@ -2,17 +2,27 @@
|
|
|
2
2
|
* `/api/routing` — configuration and the Jev key. Normal authenticated
|
|
3
3
|
* OpenChamber routes: do not add them to browser URL-token allowlists.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* The send-path rewrite, registered ahead of the generic OpenCode proxy. In
|
|
6
|
+
* OpenCode 2.x a prompt body carries no model: the client switches the session
|
|
7
|
+
* first (`POST /session/:id/model`) and sends afterwards. So the rewrite is in
|
|
8
|
+
* two halves:
|
|
9
|
+
*
|
|
10
|
+
* - `POST /session/:id/model` with `openchamber/auto` never reaches OpenCode,
|
|
11
|
+
* which has no such provider. It marks the session as routed and answers as
|
|
12
|
+
* a switch would.
|
|
13
|
+
* - `POST /session/:id/{prompt,command}` in a routed session asks Jev on the
|
|
14
|
+
* request text and switches the session onto the chosen model and agent
|
|
15
|
+
* before the prompt is forwarded.
|
|
16
|
+
*
|
|
17
|
+
* Both parse the JSON body only for JSON requests, so a streamed or non-JSON
|
|
18
|
+
* upload reaches the proxy untouched.
|
|
10
19
|
*/
|
|
11
20
|
import express from 'express';
|
|
12
|
-
import {
|
|
21
|
+
import { isAutoModel } from './defaults.js';
|
|
13
22
|
|
|
14
|
-
const
|
|
15
|
-
|
|
23
|
+
const CREATE_PATH = '/api/session';
|
|
24
|
+
const MODEL_PATH = '/api/session/:sessionId/model';
|
|
25
|
+
const SEND_PATHS = [
|
|
16
26
|
'/api/session/:sessionId/prompt',
|
|
17
27
|
'/api/session/:sessionId/command',
|
|
18
28
|
];
|
|
@@ -23,12 +33,9 @@ const sendError = (res, error) => {
|
|
|
23
33
|
};
|
|
24
34
|
|
|
25
35
|
export function registerRoutingRoutes(app, runtime) {
|
|
26
|
-
const unavailable = (res) => res.status(404).json({ error: 'Routing is not available in this build' });
|
|
27
|
-
|
|
28
36
|
app.get('/api/routing', async (_req, res) => {
|
|
29
37
|
try {
|
|
30
38
|
const state = await runtime.describe();
|
|
31
|
-
if (!state.available) return unavailable(res);
|
|
32
39
|
res.json({ ...state, heldPermissions: runtime.heldPermissions() });
|
|
33
40
|
} catch (error) {
|
|
34
41
|
sendError(res, error);
|
|
@@ -36,7 +43,6 @@ export function registerRoutingRoutes(app, runtime) {
|
|
|
36
43
|
});
|
|
37
44
|
|
|
38
45
|
app.put('/api/routing', express.json({ limit: '256kb' }), async (req, res) => {
|
|
39
|
-
if (!isRoutingFeatureAvailable()) return unavailable(res);
|
|
40
46
|
try {
|
|
41
47
|
res.json(await runtime.updateConfig(req.body?.config));
|
|
42
48
|
} catch (error) {
|
|
@@ -45,7 +51,6 @@ export function registerRoutingRoutes(app, runtime) {
|
|
|
45
51
|
});
|
|
46
52
|
|
|
47
53
|
app.put('/api/routing/token', express.json({ limit: '16kb' }), async (req, res) => {
|
|
48
|
-
if (!isRoutingFeatureAvailable()) return unavailable(res);
|
|
49
54
|
try {
|
|
50
55
|
res.json(await runtime.setToken(req.body?.token));
|
|
51
56
|
} catch (error) {
|
|
@@ -54,7 +59,6 @@ export function registerRoutingRoutes(app, runtime) {
|
|
|
54
59
|
});
|
|
55
60
|
|
|
56
61
|
app.delete('/api/routing/token', async (_req, res) => {
|
|
57
|
-
if (!isRoutingFeatureAvailable()) return unavailable(res);
|
|
58
62
|
try {
|
|
59
63
|
res.json(await runtime.clearToken());
|
|
60
64
|
} catch (error) {
|
|
@@ -65,17 +69,56 @@ export function registerRoutingRoutes(app, runtime) {
|
|
|
65
69
|
|
|
66
70
|
export function registerRoutingPromptRewrite(app, runtime) {
|
|
67
71
|
const parseJson = express.json({ limit: '50mb' });
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
|
|
73
|
+
/** Parses the body only for JSON requests; anything else stays a stream. */
|
|
74
|
+
const withParsedBody = (handler) => (req, res, next) => {
|
|
70
75
|
const contentType = String(req.headers['content-type'] ?? '').toLowerCase();
|
|
71
76
|
if (!contentType.includes('application/json')) return next();
|
|
72
77
|
parseJson(req, res, (parseError) => {
|
|
73
78
|
if (parseError) return next(parseError);
|
|
74
|
-
|
|
75
|
-
const directory = url.searchParams.get('directory') || req.get('x-opencode-directory') || undefined;
|
|
76
|
-
runtime.resolvePromptBody(req.body, { sessionId: req.params.sessionId, directory })
|
|
77
|
-
.then(() => next())
|
|
78
|
-
.catch((error) => sendError(res, error));
|
|
79
|
+
handler(req, res, next);
|
|
79
80
|
});
|
|
80
|
-
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const directoryOf = (req) => {
|
|
84
|
+
const url = new URL(req.url, 'http://localhost');
|
|
85
|
+
return url.searchParams.get('directory') || req.get('x-opencode-directory') || undefined;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Session creation is the other v2 request that carries a model: flows that
|
|
89
|
+
// know their first turn's selection create the session on it (btw forks,
|
|
90
|
+
// auto review, fusion, extension starts). OpenCode accepts the sentinel at
|
|
91
|
+
// creation and only fails the first prompt with "provider.no-route", so the
|
|
92
|
+
// model is dropped here and the session starts on OpenCode's default. No
|
|
93
|
+
// session id exists yet to mark; the first send resends the sentinel through
|
|
94
|
+
// the model switch below because the session's record never matches Auto.
|
|
95
|
+
app.post(CREATE_PATH, withParsedBody((req, _res, next) => {
|
|
96
|
+
if (!isAutoModel(req.body?.model)) return next();
|
|
97
|
+
delete req.body.model;
|
|
98
|
+
next();
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
app.post(MODEL_PATH, withParsedBody((req, res, next) => {
|
|
102
|
+
const directory = directoryOf(req);
|
|
103
|
+
// Swallowed, not forwarded: OpenCode has no `openchamber` provider, and the
|
|
104
|
+
// real model is only known once the request text arrives.
|
|
105
|
+
if (runtime.noteModelSelection(req.params.sessionId, req.body?.model, directory)) return res.status(204).end();
|
|
106
|
+
next();
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
// Whether the session is routed is known from the URL alone, so an ordinary
|
|
110
|
+
// send is never read: it reaches the proxy as the stream it arrived as, the
|
|
111
|
+
// way it did before routing existed.
|
|
112
|
+
const routedSendsOnly = (handler) => {
|
|
113
|
+
const parsed = withParsedBody(handler);
|
|
114
|
+
return (req, res, next) => (runtime.isAutoSession(req.params.sessionId) ? parsed(req, res, next) : next());
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
app.post(SEND_PATHS, routedSendsOnly((req, res, next) => {
|
|
118
|
+
const sessionId = req.params.sessionId;
|
|
119
|
+
const directory = directoryOf(req);
|
|
120
|
+
runtime.routeSend({ sessionId, directory, body: req.body })
|
|
121
|
+
.then(() => next())
|
|
122
|
+
.catch((error) => sendError(res, error));
|
|
123
|
+
}));
|
|
81
124
|
}
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Owns Jev routing at runtime: whether Auto is ready,
|
|
3
|
-
* that
|
|
4
|
-
* a permission is auto-accepted. Every failure path keeps the user's own
|
|
2
|
+
* Owns Jev routing at runtime: whether Auto is ready, which model and agent a
|
|
3
|
+
* send that selected `openchamber/auto` runs on, and the safety net consulted
|
|
4
|
+
* before a permission is auto-accepted. Every failure path keeps the user's own
|
|
5
5
|
* behaviour: a prompt goes to the fallback model, a permission is accepted as
|
|
6
6
|
* auto-accept would have, and the UI is told why.
|
|
7
|
+
*
|
|
8
|
+
* OpenCode 2.x holds the model and the agent on the session, switched by their
|
|
9
|
+
* own calls, and a prompt body carries only the user's text. So Auto is a
|
|
10
|
+
* per-session state here: the sentinel arrives on `POST /session/:id/model`
|
|
11
|
+
* and is swallowed, and every prompt in that session is routed until a real
|
|
12
|
+
* model is selected.
|
|
7
13
|
*/
|
|
8
|
-
import {
|
|
14
|
+
import { OpenCode } from '@opencode/client';
|
|
9
15
|
import { z } from 'zod';
|
|
10
|
-
import {
|
|
11
|
-
import { BUILTIN_CATEGORIES, isAutoModel } from './defaults.js';
|
|
16
|
+
import { AUTO_MODEL_REF, BUILTIN_CATEGORIES, isAutoModel } from './defaults.js';
|
|
12
17
|
import { createRoutingStore, parseEffectiveConfig } from './store.js';
|
|
13
|
-
import { buildPermissionRequest, buildRoutingRequest, createJevClient, decidePermission, decideRouting } from './jev.js';
|
|
18
|
+
import { buildPermissionRequest, buildRoutingRequest, createJevClient, decidePermission, decideRouting, jevEndpoint } from './jev.js';
|
|
14
19
|
import { loadRoutingHistory } from './history.js';
|
|
15
20
|
|
|
16
21
|
const HISTORY_TIMEOUT_MS = 2500;
|
|
@@ -19,25 +24,31 @@ const PERMISSION_DECISION_TTL_MS = 15 * 60 * 1000;
|
|
|
19
24
|
|
|
20
25
|
const errorMessage = (error) => (error instanceof Error ? error.message : String(error));
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
27
|
+
// v2's command body (`session.command` in the protocol) names the command in
|
|
28
|
+
// `name` and carries its arguments in `text`; a prompt body has no `name`.
|
|
29
|
+
const commandBodySchema = z.object({ name: z.string().trim().min(1), text: z.string().nullish() });
|
|
30
|
+
// v2 sends one user turn as flat text; the context the composer attached went
|
|
31
|
+
// ahead of it as synthetic messages, which are never the request being routed.
|
|
32
|
+
const promptBodySchema = z.object({ text: z.string().nullish() });
|
|
25
33
|
|
|
26
|
-
/** The user's words for this send:
|
|
34
|
+
/** The user's words for this send: the prompt text, or the slash command. */
|
|
27
35
|
export const requestTextOf = (body) => {
|
|
28
36
|
const command = commandBodySchema.safeParse(body);
|
|
29
37
|
if (command.success) {
|
|
30
|
-
const args = command.data.
|
|
31
|
-
return `/${command.data.
|
|
38
|
+
const args = command.data.text?.trim();
|
|
39
|
+
return `/${command.data.name}${args ? ` ${args}` : ''}`;
|
|
32
40
|
}
|
|
33
41
|
const prompt = promptBodySchema.safeParse(body);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
return (prompt.success ? prompt.data.text ?? '' : '').trim();
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const agentBodySchema = z.object({ agent: z.string().trim().min(1) });
|
|
46
|
+
|
|
47
|
+
/** OpenChamber stores a model as `{ providerID, modelID }`; v2 wants a `Model.Ref`. */
|
|
48
|
+
const toModelRef = (model, variant) => {
|
|
49
|
+
const ref = { providerID: model.providerID, id: model.modelID };
|
|
50
|
+
if (variant) ref.variant = variant;
|
|
51
|
+
return ref;
|
|
41
52
|
};
|
|
42
53
|
|
|
43
54
|
export function createRoutingRuntime({
|
|
@@ -51,6 +62,14 @@ export function createRoutingRuntime({
|
|
|
51
62
|
now = Date.now,
|
|
52
63
|
}) {
|
|
53
64
|
const permissionDecisions = new Map();
|
|
65
|
+
// Sessions the user put on Auto. The sentinel never reaches OpenCode, so
|
|
66
|
+
// nothing upstream remembers the choice for us.
|
|
67
|
+
// TODO(v2): this is process memory. A server restart drops the mark and the
|
|
68
|
+
// session silently runs on whatever model it was last switched to while the
|
|
69
|
+
// composer still shows Auto. Either persist it next to `routing.json` or have
|
|
70
|
+
// the client resend the sentinel with every send.
|
|
71
|
+
const autoSessions = new Map();
|
|
72
|
+
const AUTO_SESSION_LIMIT = 1000;
|
|
54
73
|
|
|
55
74
|
const broadcast = (type, properties) => {
|
|
56
75
|
try {
|
|
@@ -64,63 +83,82 @@ export function createRoutingRuntime({
|
|
|
64
83
|
|
|
65
84
|
/** What the client needs to decide whether to offer Auto and what the settings page shows. */
|
|
66
85
|
const describe = async () => {
|
|
67
|
-
const available = isRoutingFeatureAvailable();
|
|
68
|
-
if (!available) return { available: false, autoReady: false, tokenPresent: false, config: null, builtins: [] };
|
|
69
86
|
const [config, token] = await Promise.all([store.readConfig(), store.readToken()]);
|
|
70
87
|
const tokenPresent = Boolean(token);
|
|
71
|
-
|
|
88
|
+
// A key is not a precondition: without one Jev answers through the free
|
|
89
|
+
// model zen serves, so Auto only needs a fallback and two categories.
|
|
90
|
+
const autoReady = config.enabled && Boolean(config.fallback) && enabledCategories(config).length >= 2;
|
|
72
91
|
// Built-in text travels with the config so "Reset" in Settings restores the shipped wording.
|
|
73
|
-
|
|
92
|
+
// `available` stays in the payload for the client: a runtime without an
|
|
93
|
+
// OpenChamber server (VS Code) answers 404 and reads it as false.
|
|
94
|
+
return { available: true, autoReady, tokenPresent, jevSource: jevEndpoint(token).source, config, builtins: BUILTIN_CATEGORIES };
|
|
74
95
|
};
|
|
75
96
|
|
|
76
97
|
const publishUpdated = async () => {
|
|
77
98
|
const state = await describe();
|
|
78
|
-
broadcast('openchamber:routing.updated', {
|
|
99
|
+
broadcast('openchamber:routing.updated', {
|
|
100
|
+
available: state.available, autoReady: state.autoReady, tokenPresent: state.tokenPresent, jevSource: state.jevSource,
|
|
101
|
+
});
|
|
79
102
|
return state;
|
|
80
103
|
};
|
|
81
104
|
|
|
105
|
+
const openCodeClient = (directory) => {
|
|
106
|
+
const headers = { ...getOpenCodeAuthHeaders() };
|
|
107
|
+
// v2 scopes by header and rejects non-ASCII header values.
|
|
108
|
+
const scope = z.string().trim().min(1).safeParse(directory);
|
|
109
|
+
if (scope.success) headers['x-opencode-directory'] = encodeURIComponent(scope.data);
|
|
110
|
+
return OpenCode.make({ baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''), headers });
|
|
111
|
+
};
|
|
112
|
+
|
|
82
113
|
const readHistory = async ({ sessionId, directory }) => {
|
|
83
|
-
const
|
|
84
|
-
const client = createOpencodeClient({ baseUrl, headers: getOpenCodeAuthHeaders(), throwOnError: true });
|
|
114
|
+
const client = openCodeClient(directory);
|
|
85
115
|
const signal = AbortSignal.timeout(HISTORY_TIMEOUT_MS);
|
|
86
116
|
return loadRoutingHistory({
|
|
87
117
|
signal,
|
|
88
|
-
|
|
118
|
+
// v2 pages a session's messages newest first and returns `{ data, cursor }`.
|
|
119
|
+
readPage: ({ limit, cursor }) => client.message.list(
|
|
120
|
+
{ sessionID: sessionId, limit, ...(cursor ? { cursor } : { order: 'desc' }) },
|
|
121
|
+
{ signal },
|
|
122
|
+
),
|
|
89
123
|
});
|
|
90
124
|
};
|
|
91
125
|
|
|
92
126
|
// A category without a model of its own means "the fallback pair"; a variant
|
|
93
127
|
// only travels with the model it was chosen for.
|
|
94
|
-
const
|
|
128
|
+
const chooseSelection = (config, choice, composerAgent) => {
|
|
95
129
|
const own = Boolean(choice?.model);
|
|
96
130
|
const model = own ? choice.model : config.fallback.model;
|
|
97
131
|
const variant = own ? choice.variant : config.fallback.variant;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (choice?.agent) body.agent = choice.agent;
|
|
105
|
-
return { providerID: model.providerID, modelID: model.modelID, variant: variant ?? null, agent: choice?.agent ?? null };
|
|
132
|
+
return {
|
|
133
|
+
model: toModelRef(model, variant),
|
|
134
|
+
// A category agent replaces the composer's; an empty one keeps it.
|
|
135
|
+
agent: choice?.agent || composerAgent || null,
|
|
136
|
+
decision: { providerID: model.providerID, modelID: model.modelID, variant: variant ?? null, agent: choice?.agent ?? null },
|
|
137
|
+
};
|
|
106
138
|
};
|
|
107
139
|
|
|
108
140
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
141
|
+
* Resolves one send that named the Auto sentinel. Returns the model and
|
|
142
|
+
* agent the send must use, or null when a real model was selected.
|
|
143
|
+
*
|
|
144
|
+
* v2 carries neither model nor agent in a prompt body — they are session
|
|
145
|
+
* state, switched by their own calls — so the caller applies the selection
|
|
146
|
+
* (`applySessionSelection`, or its own switch calls) instead of the runtime
|
|
147
|
+
* rewriting a body in place the way v1 allowed.
|
|
148
|
+
*
|
|
111
149
|
* Throws only when Auto cannot be honoured at all (no fallback configured):
|
|
112
150
|
* the sentinel must never reach OpenCode.
|
|
113
151
|
*/
|
|
114
|
-
const
|
|
115
|
-
if (!isAutoModel(
|
|
152
|
+
const resolveAutoSelection = async ({ sessionId, directory, model, agent, requestText }) => {
|
|
153
|
+
if (!isAutoModel(model)) return null;
|
|
116
154
|
const state = await describe();
|
|
117
155
|
const config = state.config;
|
|
118
156
|
if (!config?.fallback) {
|
|
119
157
|
throw Object.assign(new Error('Auto routing is selected but no fallback model is configured'), { status: 400 });
|
|
120
158
|
}
|
|
121
159
|
const decision = { sessionId, at: now(), category: null, confidence: 0, reason: 'not-ready', ms: 0 };
|
|
160
|
+
let selection;
|
|
122
161
|
if (state.autoReady) {
|
|
123
|
-
const request = requestTextOf(body);
|
|
124
162
|
let history = [];
|
|
125
163
|
try {
|
|
126
164
|
history = await readHistory({ sessionId, directory });
|
|
@@ -129,23 +167,69 @@ export function createRoutingRuntime({
|
|
|
129
167
|
}
|
|
130
168
|
try {
|
|
131
169
|
const token = await store.readToken();
|
|
170
|
+
const request = (requestText ?? '').trim();
|
|
132
171
|
const { answers, ms } = await jev.ask(buildRoutingRequest({ categories: enabledCategories(config), history, request }), token);
|
|
133
172
|
const result = decideRouting(answers.category, { categories: enabledCategories(config), minConfidence: config.minConfidence });
|
|
134
173
|
decision.category = result.category?.id ?? null;
|
|
135
174
|
decision.confidence = result.confidence;
|
|
136
175
|
decision.reason = result.reason;
|
|
137
176
|
decision.ms = ms;
|
|
138
|
-
|
|
177
|
+
selection = chooseSelection(config, result.category, agent);
|
|
139
178
|
} catch (error) {
|
|
140
179
|
decision.reason = 'error';
|
|
141
180
|
decision.error = errorMessage(error);
|
|
142
|
-
|
|
181
|
+
selection = chooseSelection(config, null, agent);
|
|
143
182
|
}
|
|
144
183
|
} else {
|
|
145
|
-
|
|
184
|
+
selection = chooseSelection(config, null, agent);
|
|
146
185
|
}
|
|
186
|
+
Object.assign(decision, selection.decision);
|
|
147
187
|
broadcast('openchamber:routing.decision', decision);
|
|
148
|
-
return decision;
|
|
188
|
+
return { model: selection.model, agent: selection.agent, decision };
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* `POST /session/:id/model` with the sentinel puts the session on Auto;
|
|
193
|
+
* with any real model it takes it off again.
|
|
194
|
+
*/
|
|
195
|
+
const noteModelSelection = (sessionId, model, directory) => {
|
|
196
|
+
if (!sessionId) return false;
|
|
197
|
+
if (!isAutoModel(model)) {
|
|
198
|
+
autoSessions.delete(sessionId);
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
autoSessions.delete(sessionId);
|
|
202
|
+
autoSessions.set(sessionId, { directory: directory ?? null, at: now() });
|
|
203
|
+
while (autoSessions.size > AUTO_SESSION_LIMIT) autoSessions.delete(autoSessions.keys().next().value);
|
|
204
|
+
return true;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const isAutoSession = (sessionId) => Boolean(sessionId) && autoSessions.has(sessionId);
|
|
208
|
+
|
|
209
|
+
/** Switches the session onto a resolved selection, the way a v2 send does. */
|
|
210
|
+
const applySessionSelection = async (sessionId, directory, selection) => {
|
|
211
|
+
const client = openCodeClient(directory);
|
|
212
|
+
await client.session.switchModel({ sessionID: sessionId, model: selection.model });
|
|
213
|
+
if (selection.agent) await client.session.switchAgent({ sessionID: sessionId, agent: selection.agent });
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* One send in a routed session: asks Jev on the request text, switches the
|
|
218
|
+
* session onto the answer, and keeps the body in step with it.
|
|
219
|
+
*/
|
|
220
|
+
const routeSend = async ({ sessionId, directory, body }) => {
|
|
221
|
+
const resolved = await resolveAutoSelection({
|
|
222
|
+
sessionId,
|
|
223
|
+
directory,
|
|
224
|
+
model: AUTO_MODEL_REF,
|
|
225
|
+
agent: agentBodySchema.safeParse(body).data?.agent ?? null,
|
|
226
|
+
requestText: requestTextOf(body),
|
|
227
|
+
});
|
|
228
|
+
if (!resolved) return null;
|
|
229
|
+
// v2 prompt and command bodies carry neither model nor agent: switching
|
|
230
|
+
// the session is the whole application of the decision.
|
|
231
|
+
await applySessionSelection(sessionId, directory, resolved);
|
|
232
|
+
return resolved.decision;
|
|
149
233
|
};
|
|
150
234
|
|
|
151
235
|
/**
|
|
@@ -158,7 +242,7 @@ export function createRoutingRuntime({
|
|
|
158
242
|
const cached = permissionDecisions.get(permission.id);
|
|
159
243
|
if (cached && now() - cached.at < PERMISSION_DECISION_TTL_MS) return cached.result;
|
|
160
244
|
const state = await describe();
|
|
161
|
-
if (!state.
|
|
245
|
+
if (!state.config?.enabled || !state.config.safetyNet.enabled) return { action: 'accept' };
|
|
162
246
|
let result;
|
|
163
247
|
try {
|
|
164
248
|
const token = await store.readToken();
|
|
@@ -213,5 +297,18 @@ export function createRoutingRuntime({
|
|
|
213
297
|
return held;
|
|
214
298
|
};
|
|
215
299
|
|
|
216
|
-
return {
|
|
300
|
+
return {
|
|
301
|
+
describe,
|
|
302
|
+
noteModelSelection,
|
|
303
|
+
isAutoSession,
|
|
304
|
+
resolveAutoSelection,
|
|
305
|
+
applySessionSelection,
|
|
306
|
+
routeSend,
|
|
307
|
+
evaluatePermission,
|
|
308
|
+
forgetPermission,
|
|
309
|
+
heldPermissions,
|
|
310
|
+
updateConfig,
|
|
311
|
+
setToken,
|
|
312
|
+
clearToken,
|
|
313
|
+
};
|
|
217
314
|
}
|