@vantran-se/openchamber-web 1.24.4 → 2.0.1
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-ntXIbhV2.js} +1 -1
- package/dist/assets/DiffView-CjxUovbM.js +4 -0
- package/dist/assets/FilesView-DFWD0RQR.js +17 -0
- package/dist/assets/{GitView-Byu2DY7s.js → GitView-BWa7e5vw.js} +4 -4
- package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-DdOuyZzp.js} +1 -1
- package/dist/assets/GuestSurfacePane-DDfzid3U.js +1 -0
- package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-3sGwqexh.js} +1 -1
- package/dist/assets/LinearIssuesView-C6dpIdjw.js +1 -0
- package/dist/assets/MarkdownImageGallery-Q7YR_tl4.js +1 -0
- package/dist/assets/MarkdownRendererImpl-DxDV56bL.js +12 -0
- package/dist/assets/OnboardingScreen-rK4Stg__.js +1 -0
- package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-DS-IMU1j.js} +1 -1
- package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-DW23KWKZ.js} +1 -1
- package/dist/assets/PlanView-BCk_VobP.js +5 -0
- package/dist/assets/PluginPane-BPP1dbck.js +3 -0
- package/dist/assets/SettingsView-kMmrpn43.js +29 -0
- package/dist/assets/SettingsWindow-BpvD6IYQ.js +1 -0
- package/dist/assets/SubmoduleDiffSummary-BLttSS1X.js +1 -0
- package/dist/assets/ToolOutputDialog-CtqDqbyr.js +15 -0
- package/dist/assets/ToolPartDiffPreview-5FcKdyRy.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-C763hNCR.js} +4 -4
- package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-lap5btCF.js} +2 -2
- package/dist/assets/appThemeRegistry-u4zw1l0B.js +1 -0
- package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-CwGJfJG-.js} +1 -1
- package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-DW5jjX4n.js} +1 -1
- package/dist/assets/de-CndEg67T.js +10 -0
- package/dist/assets/es-DglvPlaC.js +16 -0
- package/dist/assets/fr-DejPCpbS.js +14 -0
- package/dist/assets/index-C2r64QfD.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-D47a6hMP.js} +1 -1
- package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-B-mGgeKl.js} +1 -1
- package/dist/assets/ja-CVpQawCd.js +16 -0
- package/dist/assets/ko-Bw446h9j.js +16 -0
- package/dist/assets/main-Bz4WWXbr.js +2 -0
- package/dist/assets/main-oreqDsOV.js +896 -0
- package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-D2e5sp9y.js} +4 -4
- package/dist/assets/miniChat-D1v2gsBI.js +2 -0
- package/dist/assets/mobile-DBj3lCf2.js +2 -0
- package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-BpeUOV91.js} +3 -3
- package/dist/assets/pl-BYNlPdhm.js +16 -0
- package/dist/assets/pt-BR-CL-Yfv9H.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-DVtfEB2z.js +2 -0
- package/dist/assets/renderMobileApp-DFp9-xbG.js +25 -0
- package/dist/assets/runtimeEndpointReset-CsIpUep1.js +27 -0
- package/dist/assets/scheduledTasksApi-B5OHVpPM.js +1 -0
- package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-DNm2Z_Jc.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-DGF2kG7g.js} +1 -1
- package/dist/assets/tr-DzmbxLy-.js +7 -0
- package/dist/assets/uk-C7vQnwro.js +16 -0
- package/dist/assets/useAppFontEffects-Ci3FEaPf.js +7337 -0
- package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CuUuakrt.js} +2 -2
- package/dist/assets/useRootScrollLock-CQIsyERq.js +2 -0
- package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-CQa7_1cr.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-B6tKQyJN.js} +2 -2
- package/dist/assets/zh-CN-Cl0srS-z.js +16 -0
- package/dist/assets/zh-TW-DtII5bDb.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,10 +1,17 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
createWorktree as createWorktreeDefault,
|
|
4
|
+
getWorktreeBootstrapStatus as getWorktreeBootstrapStatusDefault,
|
|
5
|
+
resolvePrimaryWorktreeRoot,
|
|
6
|
+
} from '../git/index.js';
|
|
4
7
|
import { expandSnippets } from '../opencode/snippets.js';
|
|
5
|
-
import {
|
|
8
|
+
import { AUTO_MODEL_REF, isAutoModel } from '../routing/defaults.js';
|
|
9
|
+
import { parseScheduledCommandPrompt } from '../scheduled-tasks/runtime.js';
|
|
6
10
|
import { buildGoalIntroText, createSessionGoal } from '../session-goal/create.js';
|
|
7
11
|
import { OpenChamberControlError, asControlError } from '../openchamber-control/error.js';
|
|
12
|
+
import { createArchiveStore } from './archive-store.js';
|
|
13
|
+
import { createOpenCodeClient as defaultCreateOpenCodeClient } from './opencode-client.js';
|
|
14
|
+
import { createSessionMetadataStore, createOpenCodeSessionMetadata } from './session-metadata-store.js';
|
|
8
15
|
|
|
9
16
|
const asNonEmptyString = (value) => {
|
|
10
17
|
if (typeof value !== 'string') return null;
|
|
@@ -12,6 +19,8 @@ const asNonEmptyString = (value) => {
|
|
|
12
19
|
return trimmed.length > 0 ? trimmed : null;
|
|
13
20
|
};
|
|
14
21
|
|
|
22
|
+
const asList = (value) => (Array.isArray(value) ? value : []);
|
|
23
|
+
|
|
15
24
|
const splitModel = (value) => {
|
|
16
25
|
const model = asNonEmptyString(value);
|
|
17
26
|
if (!model) return null;
|
|
@@ -59,29 +68,33 @@ const resolveGoalInput = (payload, prompt) => {
|
|
|
59
68
|
|
|
60
69
|
const isPrimaryAgentMode = (mode) => !mode || mode === 'primary' || mode === 'all';
|
|
61
70
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
// OpenCode 2.x serves one flat model catalogue instead of models nested under
|
|
72
|
+
// providers: every entry already names its provider.
|
|
73
|
+
const hasCatalogModel = (models, providerID, modelID) => models.some(
|
|
74
|
+
(model) => model?.providerID === providerID && model?.modelID === modelID,
|
|
75
|
+
);
|
|
67
76
|
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
};
|
|
77
|
+
const findCatalogModel = (models, providerID, modelID) => models.find(
|
|
78
|
+
(model) => model?.providerID === providerID && model?.modelID === modelID,
|
|
79
|
+
) || null;
|
|
72
80
|
|
|
73
|
-
const resolveVariant = (
|
|
81
|
+
const resolveVariant = (models, providerID, modelID, variant) => {
|
|
74
82
|
const normalized = asNonEmptyString(variant);
|
|
75
83
|
if (!normalized) return undefined;
|
|
76
|
-
const
|
|
77
|
-
|
|
84
|
+
const model = findCatalogModel(models, providerID, modelID);
|
|
85
|
+
// A model the catalog does not know yet (cold or unreachable) keeps the
|
|
86
|
+
// user's saved variant instead of losing it to a discovery gap.
|
|
78
87
|
if (!model) return normalized;
|
|
79
|
-
return model
|
|
80
|
-
? normalized
|
|
81
|
-
: undefined;
|
|
88
|
+
return asList(model.variants).some((entry) => entry?.id === normalized) ? normalized : undefined;
|
|
82
89
|
};
|
|
83
90
|
|
|
84
|
-
|
|
91
|
+
// Config `model` is either "providerID/modelID" or the expanded object form.
|
|
92
|
+
const parseConfigModel = (value) => {
|
|
93
|
+
if (typeof value === 'string') return splitModel(value);
|
|
94
|
+
const providerID = asNonEmptyString(value?.providerID);
|
|
95
|
+
const modelID = asNonEmptyString(value?.model);
|
|
96
|
+
return providerID && modelID ? { providerID, modelID } : null;
|
|
97
|
+
};
|
|
85
98
|
|
|
86
99
|
const resolveProjectDefaults = (settings, directory, projectId) => {
|
|
87
100
|
const projects = Array.isArray(settings?.projects) ? settings.projects : [];
|
|
@@ -95,181 +108,161 @@ const resolveProjectDefaults = (settings, directory, projectId) => {
|
|
|
95
108
|
};
|
|
96
109
|
};
|
|
97
110
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const fetchJson = async (url, authHeaders, fallback, directory) => {
|
|
106
|
-
const response = await fetch(url.toString(), {
|
|
107
|
-
headers: { ...authHeaders, ...buildDirectoryHeaders(directory), accept: 'application/json' },
|
|
108
|
-
});
|
|
109
|
-
if (!response.ok) return fallback;
|
|
110
|
-
return response.json().catch(() => fallback);
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const fetchSelectionInputs = async ({ buildOpenCodeUrl, authHeaders, directory, readSettingsFromDiskMigrated }) => {
|
|
111
|
+
/** `x-opencode-directory` is how v2 scopes a request; there is no query param. */
|
|
112
|
+
/**
|
|
113
|
+
* Everything the default model/agent resolution needs, from one directory-scoped
|
|
114
|
+
* client. A failed lookup answers empty on purpose: an empty catalogue means
|
|
115
|
+
* "unknown", and callers must never turn that into a rejection.
|
|
116
|
+
*/
|
|
117
|
+
const fetchSelectionInputs = async ({ client, readSettingsFromDiskMigrated }) => {
|
|
114
118
|
const settings = await readSettingsFromDiskMigrated();
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const [providersBody, agentsBody, configBody] = await Promise.all([
|
|
123
|
-
fetchJson(providersUrl, authHeaders, { providers: [] }, directory),
|
|
124
|
-
fetchJson(agentsUrl, authHeaders, [], directory),
|
|
125
|
-
fetchJson(configUrl, authHeaders, {}, directory),
|
|
119
|
+
const [models, agents, configEntries] = await Promise.all([
|
|
120
|
+
client.model.list().then((response) => asList(response?.data)).catch(() => []),
|
|
121
|
+
// v2 agents carry `id` (`build`, what prompts and sessions refer to) and a
|
|
122
|
+
// display `name` (`Build`); every lookup here is by id.
|
|
123
|
+
client.agent.list().then((response) => asList(response?.data)).catch(() => []),
|
|
124
|
+
client.config.get().then((response) => asList(response)).catch(() => []),
|
|
126
125
|
]);
|
|
127
126
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
127
|
+
// Config entries arrive lowest priority first, so the last definition wins.
|
|
128
|
+
let opencodeDefaultAgent = null;
|
|
129
|
+
let opencodeDefaultModel = null;
|
|
130
|
+
for (const entry of configEntries) {
|
|
131
|
+
const info = entry?.info;
|
|
132
|
+
if (!info) continue;
|
|
133
|
+
const agent = asNonEmptyString(info.default_agent);
|
|
134
|
+
if (agent) opencodeDefaultAgent = agent;
|
|
135
|
+
const model = parseConfigModel(info.model);
|
|
136
|
+
if (model) opencodeDefaultModel = model;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { settings, models, agents, opencodeDefaultAgent, opencodeDefaultModel };
|
|
135
140
|
};
|
|
136
141
|
|
|
137
|
-
const resolveDefaultSelection = ({ agents,
|
|
142
|
+
const resolveDefaultSelection = ({ agents, models, settings, projectDefaults, opencodeDefaultAgent, opencodeDefaultModel }) => {
|
|
138
143
|
const primaryAgents = agents.filter((agent) => isPrimaryAgentMode(agent?.mode) && agent?.hidden !== true);
|
|
139
144
|
let resolvedAgent = null;
|
|
140
145
|
const projectDefaultAgent = asNonEmptyString(projectDefaults?.defaultAgent);
|
|
141
146
|
const settingsDefaultAgent = asNonEmptyString(settings?.defaultAgent);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
// The project's default agent wins over the global one. v1 stored the agent's
|
|
148
|
+
// display name; v2 agents are addressed by id (`build` vs `Build`), so a
|
|
149
|
+
// setting saved before the upgrade still resolves.
|
|
150
|
+
const findAgentBySetting = (wantedName) => {
|
|
151
|
+
const wanted = wantedName.toLowerCase();
|
|
152
|
+
return agents.find((agent) => agent?.id === wantedName)
|
|
153
|
+
|| agents.find((agent) => typeof agent?.name === 'string' && agent.name.toLowerCase() === wanted)
|
|
154
|
+
|| agents.find((agent) => typeof agent?.id === 'string' && agent.id.toLowerCase() === wanted)
|
|
155
|
+
|| null;
|
|
156
|
+
};
|
|
157
|
+
if (projectDefaultAgent) resolvedAgent = findAgentBySetting(projectDefaultAgent);
|
|
158
|
+
if (!resolvedAgent && settingsDefaultAgent) resolvedAgent = findAgentBySetting(settingsDefaultAgent);
|
|
148
159
|
if (!resolvedAgent && opencodeDefaultAgent) {
|
|
149
|
-
const candidate = agents.find((agent) => agent?.
|
|
160
|
+
const candidate = agents.find((agent) => agent?.id === opencodeDefaultAgent) || null;
|
|
150
161
|
if (candidate && isPrimaryAgentMode(candidate.mode) && candidate.hidden !== true) {
|
|
151
162
|
resolvedAgent = candidate;
|
|
152
163
|
}
|
|
153
164
|
}
|
|
154
165
|
if (!resolvedAgent) {
|
|
155
|
-
resolvedAgent = primaryAgents.find((agent) => agent?.
|
|
166
|
+
resolvedAgent = primaryAgents.find((agent) => agent?.id === 'build') || primaryAgents[0] || agents[0] || null;
|
|
156
167
|
}
|
|
157
168
|
|
|
158
169
|
let model = null;
|
|
159
170
|
let variant;
|
|
160
171
|
const projectDefaultModel = parseConfigModel(projectDefaults?.defaultModel);
|
|
161
172
|
const settingsDefaultModel = parseConfigModel(settings?.defaultModel);
|
|
173
|
+
// A saved choice is honoured even when the catalog has not listed it yet: a
|
|
174
|
+
// discovery gap must not silently move the user onto another model.
|
|
162
175
|
if (projectDefaultModel) {
|
|
163
176
|
model = projectDefaultModel;
|
|
164
|
-
variant = resolveVariant(
|
|
177
|
+
variant = resolveVariant(models, model.providerID, model.modelID, projectDefaults?.defaultVariant);
|
|
165
178
|
}
|
|
166
179
|
if (!model && settingsDefaultModel) {
|
|
167
180
|
model = settingsDefaultModel;
|
|
168
|
-
variant = resolveVariant(
|
|
181
|
+
variant = resolveVariant(models, model.providerID, model.modelID, settings?.defaultVariant);
|
|
169
182
|
}
|
|
170
183
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
184
|
+
// An agent's model is a v2 `ModelRef`: `id` is the model id, not a composite.
|
|
185
|
+
const agentModel = resolvedAgent?.model;
|
|
186
|
+
if (!model && asNonEmptyString(agentModel?.providerID) && asNonEmptyString(agentModel?.id)) {
|
|
187
|
+
model = { providerID: agentModel.providerID, modelID: agentModel.id };
|
|
188
|
+
variant = resolveVariant(models, model.providerID, model.modelID, agentModel.variant);
|
|
174
189
|
}
|
|
175
190
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
model = opencodeModel;
|
|
191
|
+
if (!model && opencodeDefaultModel) {
|
|
192
|
+
model = opencodeDefaultModel;
|
|
179
193
|
}
|
|
180
194
|
|
|
181
|
-
if (!model &&
|
|
195
|
+
if (!model && hasCatalogModel(models, FALLBACK_PROVIDER_ID, FALLBACK_MODEL_ID)) {
|
|
182
196
|
model = { providerID: FALLBACK_PROVIDER_ID, modelID: FALLBACK_MODEL_ID };
|
|
183
197
|
}
|
|
184
198
|
|
|
185
199
|
if (!model) {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
model = { providerID: provider.id, modelID: firstModel.id };
|
|
200
|
+
const first = models[0];
|
|
201
|
+
if (asNonEmptyString(first?.providerID) && asNonEmptyString(first?.modelID)) {
|
|
202
|
+
model = { providerID: first.providerID, modelID: first.modelID };
|
|
190
203
|
}
|
|
191
204
|
}
|
|
192
205
|
|
|
193
206
|
return {
|
|
194
|
-
agent: resolvedAgent?.
|
|
207
|
+
agent: resolvedAgent?.id,
|
|
195
208
|
model,
|
|
196
209
|
variant,
|
|
197
210
|
};
|
|
198
211
|
};
|
|
199
212
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
body: JSON.stringify(payload),
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
if (!response.ok) {
|
|
215
|
-
const body = await response.text().catch(() => '');
|
|
216
|
-
throw new Error(`prompt_async failed (${response.status})${body ? `: ${body}` : ''}`);
|
|
213
|
+
/**
|
|
214
|
+
* v2 selects model and agent per session, not per prompt: the choice is
|
|
215
|
+
* switched once and then persists, so every dispatch sets it explicitly rather
|
|
216
|
+
* than passing it alongside the prompt.
|
|
217
|
+
*/
|
|
218
|
+
const applySessionSelection = async ({ client, sessionID, model, agent, variant }) => {
|
|
219
|
+
if (model) {
|
|
220
|
+
await client.session.switchModel({
|
|
221
|
+
sessionID,
|
|
222
|
+
model: { id: model.modelID, providerID: model.providerID, ...(variant ? { variant } : {}) },
|
|
223
|
+
});
|
|
217
224
|
}
|
|
225
|
+
if (agent) await client.session.switchAgent({ sessionID, agent });
|
|
218
226
|
};
|
|
219
227
|
|
|
220
|
-
const createSession = async ({
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
method: 'POST',
|
|
225
|
-
headers: {
|
|
226
|
-
...authHeaders,
|
|
227
|
-
...buildDirectoryHeaders(directory),
|
|
228
|
-
'content-type': 'application/json',
|
|
229
|
-
accept: 'application/json',
|
|
230
|
-
},
|
|
231
|
-
body: JSON.stringify({ directory, ...(title ? { title } : {}) }),
|
|
228
|
+
const createSession = async ({ client, directory, title }) => {
|
|
229
|
+
const session = await client.session.create({
|
|
230
|
+
location: { directory },
|
|
231
|
+
...(title ? { title } : {}),
|
|
232
232
|
});
|
|
233
|
-
|
|
234
|
-
if (!
|
|
235
|
-
const body = await response.text().catch(() => '');
|
|
236
|
-
throw new Error(`session create failed (${response.status})${body ? `: ${body}` : ''}`);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const body = await response.json().catch(() => null);
|
|
240
|
-
const sessionID = body?.id || body?.data?.id;
|
|
241
|
-
if (!sessionID) {
|
|
242
|
-
throw new Error('failed to create session');
|
|
243
|
-
}
|
|
233
|
+
const sessionID = asNonEmptyString(session?.id);
|
|
234
|
+
if (!sessionID) throw new Error('failed to create session');
|
|
244
235
|
return sessionID;
|
|
245
236
|
};
|
|
246
237
|
|
|
247
|
-
const forkSession = async ({ client, sessionID,
|
|
248
|
-
const
|
|
238
|
+
const forkSession = async ({ client, sessionID, messageID }) => {
|
|
239
|
+
const session = await client.session.fork({
|
|
249
240
|
sessionID,
|
|
250
|
-
|
|
251
|
-
|
|
241
|
+
// OpenCode 2.0.8 replaced the SessionForkBoundary object with an optional
|
|
242
|
+
// `before` message id. Omitting it carries the whole session over, which is
|
|
243
|
+
// what the old `{ type: 'through' }` boundary meant.
|
|
244
|
+
...(messageID ? { before: messageID } : {}),
|
|
252
245
|
});
|
|
253
|
-
|
|
254
|
-
if (!session?.id) {
|
|
255
|
-
throw new Error('failed to fork session');
|
|
256
|
-
}
|
|
246
|
+
if (!asNonEmptyString(session?.id)) throw new Error('failed to fork session');
|
|
257
247
|
return session;
|
|
258
248
|
};
|
|
259
249
|
|
|
260
|
-
const
|
|
261
|
-
|
|
250
|
+
const listMessages = async ({ client, sessionID, limit }) => {
|
|
251
|
+
const response = await client.message.list({ sessionID, limit, order: 'desc' });
|
|
252
|
+
return asList(response?.data);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const latestCompletedAssistantMessageID = async ({ client, sessionID }) => {
|
|
256
|
+
let messages;
|
|
262
257
|
try {
|
|
263
|
-
|
|
258
|
+
messages = await listMessages({ client, sessionID, limit: 100 });
|
|
264
259
|
} catch {
|
|
265
260
|
return null;
|
|
266
261
|
}
|
|
267
|
-
const messages = Array.isArray(response?.data) ? response.data : [];
|
|
268
262
|
let latest = null;
|
|
269
263
|
for (const message of messages) {
|
|
270
|
-
|
|
271
|
-
if (
|
|
272
|
-
if (!latest || (info.time.created || 0) >= (latest.time?.created || 0)) latest = info;
|
|
264
|
+
if (message?.type !== 'assistant' || !Number.isFinite(message?.time?.completed)) continue;
|
|
265
|
+
if (!latest || (message.time.created || 0) >= (latest.time?.created || 0)) latest = message;
|
|
273
266
|
}
|
|
274
267
|
return asNonEmptyString(latest?.id);
|
|
275
268
|
};
|
|
@@ -277,14 +270,12 @@ const latestCompletedAssistantMessageID = async ({ client, sessionID, directory
|
|
|
277
270
|
/**
|
|
278
271
|
* Upper bound on one archive batch.
|
|
279
272
|
*
|
|
280
|
-
* The batch is
|
|
281
|
-
*
|
|
282
|
-
* more sessions than this send several batches and keep their own partial
|
|
283
|
-
* results.
|
|
273
|
+
* The batch is a bounded amount of work on one request, and callers with more
|
|
274
|
+
* sessions than this send several batches and keep their own partial results.
|
|
284
275
|
*/
|
|
285
276
|
const MAX_ARCHIVE_BATCH = 500;
|
|
286
277
|
|
|
287
|
-
const
|
|
278
|
+
const parseIdBatch = (payload) => {
|
|
288
279
|
const rawIds = payload?.ids;
|
|
289
280
|
if (!Array.isArray(rawIds) || rawIds.length === 0) {
|
|
290
281
|
return { ok: false, error: 'ids must be a non-empty array of session ids' };
|
|
@@ -299,13 +290,19 @@ const parseArchiveRequest = (payload) => {
|
|
|
299
290
|
if (!id) return { ok: false, error: 'ids must contain non-empty session ids' };
|
|
300
291
|
ids.push(id);
|
|
301
292
|
}
|
|
293
|
+
return { ok: true, ids };
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const parseArchiveRequest = (payload) => {
|
|
297
|
+
const parsed = parseIdBatch(payload);
|
|
298
|
+
if (!parsed.ok) return parsed;
|
|
302
299
|
|
|
303
300
|
const archivedAt = payload?.archivedAt;
|
|
304
301
|
if (archivedAt !== undefined && (!Number.isSafeInteger(archivedAt) || archivedAt <= 0)) {
|
|
305
302
|
return { ok: false, error: 'archivedAt must be a positive integer timestamp' };
|
|
306
303
|
}
|
|
307
304
|
|
|
308
|
-
return { ok: true, ids, archivedAt: archivedAt ?? Date.now() };
|
|
305
|
+
return { ok: true, ids: parsed.ids, archivedAt: archivedAt ?? Date.now() };
|
|
309
306
|
};
|
|
310
307
|
|
|
311
308
|
const resolveRequestedDirectory = async ({ payload, readSettingsFromDiskMigrated, sanitizeProjects, validateDirectoryPath }) => {
|
|
@@ -336,9 +333,6 @@ const resolveRequestedDirectory = async ({ payload, readSettingsFromDiskMigrated
|
|
|
336
333
|
return { ok: true, directory: validated.directory, ...(project ? { projectId: project.id } : {}) };
|
|
337
334
|
};
|
|
338
335
|
|
|
339
|
-
const PROMPT_LANDED_TIMEOUT_MS = 5_000;
|
|
340
|
-
const PROMPT_LANDED_POLL_MS = 150;
|
|
341
|
-
|
|
342
336
|
// createWorktree returns while the worktree is still being populated in the
|
|
343
337
|
// background (git reset --hard after a --no-checkout add). Dispatching a
|
|
344
338
|
// prompt into a half-populated directory makes opencode's run die with
|
|
@@ -348,54 +342,6 @@ const PROMPT_LANDED_POLL_MS = 150;
|
|
|
348
342
|
const WORKTREE_BOOTSTRAP_TIMEOUT_MS = 60_000;
|
|
349
343
|
const WORKTREE_BOOTSTRAP_POLL_MS = 150;
|
|
350
344
|
|
|
351
|
-
const waitForWorktreeBootstrapReady = async ({ directory }) => {
|
|
352
|
-
const deadline = Date.now() + WORKTREE_BOOTSTRAP_TIMEOUT_MS;
|
|
353
|
-
for (;;) {
|
|
354
|
-
const status = await getWorktreeBootstrapStatus(directory);
|
|
355
|
-
if (status?.status === 'failed') {
|
|
356
|
-
throw new OpenChamberControlError(`Worktree bootstrap failed: ${status.error || 'unknown error'}`, 500);
|
|
357
|
-
}
|
|
358
|
-
const phase = status?.phase;
|
|
359
|
-
if (status?.status === 'ready' || phase === 'git-ready' || phase === 'setup-ready') return;
|
|
360
|
-
if (Date.now() >= deadline) {
|
|
361
|
-
throw new OpenChamberControlError('Timed out waiting for the worktree bootstrap', 500);
|
|
362
|
-
}
|
|
363
|
-
await new Promise((resolve) => setTimeout(resolve, WORKTREE_BOOTSTRAP_POLL_MS));
|
|
364
|
-
}
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
const latestUserMessageID = async ({ client, sessionID, directory }) => {
|
|
368
|
-
let response;
|
|
369
|
-
try {
|
|
370
|
-
response = await client.session.messages({ sessionID, directory, limit: 100 });
|
|
371
|
-
} catch {
|
|
372
|
-
return { ok: false, messageID: null };
|
|
373
|
-
}
|
|
374
|
-
const messages = Array.isArray(response?.data) ? response.data : [];
|
|
375
|
-
let latest = null;
|
|
376
|
-
for (const message of messages) {
|
|
377
|
-
const info = message?.info;
|
|
378
|
-
if (info?.role !== 'user') continue;
|
|
379
|
-
if (!latest || (info.time?.created || 0) >= (latest.time?.created || 0)) latest = info;
|
|
380
|
-
}
|
|
381
|
-
return { ok: true, messageID: asNonEmptyString(latest?.id) };
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
// `prompt_async` answers 204 as soon as OpenCode forks the run, and every later
|
|
385
|
-
// failure is reported only on the session event stream. Confirm the prompt was
|
|
386
|
-
// actually recorded so `promptDispatched` never claims a dispatch that vanished.
|
|
387
|
-
const waitForPromptLanded = async ({ client, sessionID, directory, baselineUserMessageID }) => {
|
|
388
|
-
const deadline = Date.now() + PROMPT_LANDED_TIMEOUT_MS;
|
|
389
|
-
for (;;) {
|
|
390
|
-
const latest = await latestUserMessageID({ client, sessionID, directory });
|
|
391
|
-
// A failed lookup is not authoritative evidence that the prompt was lost.
|
|
392
|
-
if (!latest.ok) return true;
|
|
393
|
-
if (latest.messageID && latest.messageID !== baselineUserMessageID) return true;
|
|
394
|
-
if (Date.now() >= deadline) return false;
|
|
395
|
-
await new Promise((resolve) => setTimeout(resolve, PROMPT_LANDED_POLL_MS));
|
|
396
|
-
}
|
|
397
|
-
};
|
|
398
|
-
|
|
399
345
|
const resolveWorktreeInput = (payload) => {
|
|
400
346
|
if (!payload?.worktree || typeof payload.worktree !== 'object') return null;
|
|
401
347
|
const name = asNonEmptyString(payload.worktree.name);
|
|
@@ -420,54 +366,95 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
420
366
|
getOpenCodeAuthHeaders,
|
|
421
367
|
waitForOpenCodeReady,
|
|
422
368
|
emitSessionCreatedEvent,
|
|
369
|
+
broadcastGlobalUiEvent,
|
|
423
370
|
createSessionGoal: createSessionGoalOverride,
|
|
424
371
|
sessionKnowledgeRuntime = null,
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
372
|
+
dataDir = null,
|
|
373
|
+
archiveStore: injectedArchiveStore = null,
|
|
374
|
+
sessionMetadataStore: injectedSessionMetadataStore = null,
|
|
375
|
+
// Injected by the server so every metadata write takes the same path:
|
|
376
|
+
// store, broadcast, and tell the goal loop. Falls back to store+broadcast
|
|
377
|
+
// when it is absent, which is what module tests use.
|
|
378
|
+
persistSessionMetadata = null,
|
|
379
|
+
createOpenCodeClient = defaultCreateOpenCodeClient,
|
|
380
|
+
createWorktree = createWorktreeDefault,
|
|
381
|
+
getWorktreeBootstrapStatus = getWorktreeBootstrapStatusDefault,
|
|
382
|
+
// Auto routing. Sessions dispatched here talk to OpenCode through the SDK,
|
|
383
|
+
// not through the proxy that intercepts the Auto sentinel, so a default of
|
|
384
|
+
// `openchamber/auto` (Session Defaults) is resolved here before the
|
|
385
|
+
// session is switched onto it. Null when routing is not wired in.
|
|
386
|
+
resolveAutoSelection = null,
|
|
429
387
|
} = dependencies;
|
|
430
388
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
389
|
+
if ((!injectedArchiveStore || !injectedSessionMetadataStore) && !dataDir) {
|
|
390
|
+
throw new Error('openchamber session routes need either both stores or a dataDir');
|
|
391
|
+
}
|
|
392
|
+
const archiveStore = injectedArchiveStore || createArchiveStore({ dataDir });
|
|
393
|
+
const sessionMetadataStore = injectedSessionMetadataStore || createSessionMetadataStore({
|
|
394
|
+
dataDir,
|
|
395
|
+
openCode: createOpenCodeSessionMetadata({
|
|
396
|
+
buildOpenCodeUrl,
|
|
397
|
+
getOpenCodeAuthHeaders,
|
|
398
|
+
createOpenCodeClient,
|
|
399
|
+
}),
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const openCodeBaseUrl = () => buildOpenCodeUrl('/', '').replace(/\/$/, '');
|
|
403
|
+
const clientFor = (directory) => createOpenCodeClient({
|
|
404
|
+
baseUrl: openCodeBaseUrl(),
|
|
405
|
+
headers: getOpenCodeAuthHeaders(),
|
|
406
|
+
directory,
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const waitForWorktreeBootstrapReady = async ({ directory }) => {
|
|
410
|
+
const deadline = Date.now() + WORKTREE_BOOTSTRAP_TIMEOUT_MS;
|
|
411
|
+
for (;;) {
|
|
412
|
+
const status = await getWorktreeBootstrapStatus(directory);
|
|
413
|
+
if (status?.status === 'failed') {
|
|
414
|
+
throw new OpenChamberControlError(`Worktree bootstrap failed: ${status.error || 'unknown error'}`, 500);
|
|
448
415
|
}
|
|
416
|
+
const phase = status?.phase;
|
|
417
|
+
if (status?.status === 'ready' || phase === 'git-ready' || phase === 'setup-ready') return;
|
|
418
|
+
if (Date.now() >= deadline) {
|
|
419
|
+
throw new OpenChamberControlError('Timed out waiting for the worktree bootstrap', 500);
|
|
420
|
+
}
|
|
421
|
+
await new Promise((resolve) => setTimeout(resolve, WORKTREE_BOOTSTRAP_POLL_MS));
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* The selection an existing session already runs on. v2 keeps it on the
|
|
427
|
+
* session record, so there is no need to walk the message history for it.
|
|
428
|
+
*/
|
|
429
|
+
const fetchSessionSelection = async ({ client, sessionID }) => {
|
|
430
|
+
try {
|
|
431
|
+
const session = await client.session.get({ sessionID });
|
|
432
|
+
const providerID = asNonEmptyString(session?.model?.providerID);
|
|
433
|
+
const modelID = asNonEmptyString(session?.model?.id);
|
|
434
|
+
return {
|
|
435
|
+
model: providerID && modelID ? { providerID, modelID } : null,
|
|
436
|
+
agent: asNonEmptyString(session?.agent),
|
|
437
|
+
variant: asNonEmptyString(session?.model?.variant),
|
|
438
|
+
};
|
|
449
439
|
} catch {
|
|
440
|
+
return null;
|
|
450
441
|
}
|
|
451
|
-
return null;
|
|
452
442
|
};
|
|
453
443
|
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
//
|
|
444
|
+
// An unknown agent or model makes the run fail after the prompt is accepted,
|
|
445
|
+
// leaving a session with no answer. Reject them before any session, worktree,
|
|
446
|
+
// or goal side effect happens.
|
|
457
447
|
const validateRequestedSelection = async ({ directory, requestedModel, requestedAgent, requestedVariant }) => {
|
|
458
448
|
if (!requestedModel && !requestedAgent && !requestedVariant) return;
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
buildOpenCodeUrl,
|
|
462
|
-
authHeaders,
|
|
463
|
-
directory,
|
|
449
|
+
const { models, agents } = await fetchSelectionInputs({
|
|
450
|
+
client: clientFor(directory),
|
|
464
451
|
readSettingsFromDiskMigrated,
|
|
465
452
|
});
|
|
466
453
|
|
|
467
454
|
// An empty list means the lookup failed or returned nothing authoritative;
|
|
468
455
|
// it must not turn a valid selection into a rejection.
|
|
469
456
|
if (requestedAgent && agents.length > 0) {
|
|
470
|
-
const agent = agents.find((entry) => entry?.
|
|
457
|
+
const agent = agents.find((entry) => entry?.id === requestedAgent) || null;
|
|
471
458
|
if (!agent) {
|
|
472
459
|
throw new OpenChamberControlError(`Unknown agent '${requestedAgent}' for ${directory}`, 400);
|
|
473
460
|
}
|
|
@@ -476,15 +463,15 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
476
463
|
}
|
|
477
464
|
}
|
|
478
465
|
|
|
479
|
-
if (requestedModel &&
|
|
480
|
-
if (!
|
|
466
|
+
if (requestedModel && models.length > 0) {
|
|
467
|
+
if (!hasCatalogModel(models, requestedModel.providerID, requestedModel.modelID)) {
|
|
481
468
|
throw new OpenChamberControlError(
|
|
482
469
|
`Unknown model '${requestedModel.providerID}/${requestedModel.modelID}' for ${directory}`,
|
|
483
470
|
400,
|
|
484
471
|
);
|
|
485
472
|
}
|
|
486
473
|
if (requestedVariant
|
|
487
|
-
&& !resolveVariant(
|
|
474
|
+
&& !resolveVariant(models, requestedModel.providerID, requestedModel.modelID, requestedVariant)) {
|
|
488
475
|
throw new OpenChamberControlError(
|
|
489
476
|
`Unknown variant '${requestedVariant}' for model '${requestedModel.providerID}/${requestedModel.modelID}'`,
|
|
490
477
|
400,
|
|
@@ -511,7 +498,7 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
511
498
|
let agent = requestedAgent;
|
|
512
499
|
let variant = requestedVariant;
|
|
513
500
|
if (reuseSessionSelection && (!model || !agent)) {
|
|
514
|
-
const previous = await
|
|
501
|
+
const previous = await fetchSessionSelection({ client, sessionID });
|
|
515
502
|
if (previous) {
|
|
516
503
|
if (!model && previous.model) {
|
|
517
504
|
model = previous.model;
|
|
@@ -521,12 +508,7 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
521
508
|
}
|
|
522
509
|
}
|
|
523
510
|
if (!model || !agent) {
|
|
524
|
-
const inputs = await fetchSelectionInputs({
|
|
525
|
-
buildOpenCodeUrl,
|
|
526
|
-
authHeaders,
|
|
527
|
-
directory,
|
|
528
|
-
readSettingsFromDiskMigrated,
|
|
529
|
-
});
|
|
511
|
+
const inputs = await fetchSelectionInputs({ client, readSettingsFromDiskMigrated });
|
|
530
512
|
const defaults = resolveDefaultSelection({
|
|
531
513
|
...inputs,
|
|
532
514
|
projectDefaults: resolveProjectDefaults(inputs.settings, directory, projectId),
|
|
@@ -544,31 +526,53 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
544
526
|
}
|
|
545
527
|
|
|
546
528
|
const expandedPrompt = expandSnippets(prompt, directory);
|
|
529
|
+
if (isAutoModel(model)) {
|
|
530
|
+
// The sentinel must never reach OpenCode: neither the goal record nor the
|
|
531
|
+
// session switch below may carry it.
|
|
532
|
+
if (!resolveAutoSelection) {
|
|
533
|
+
throw new OpenChamberControlError('Auto routing is not available on this server. Choose a model.', 400);
|
|
534
|
+
}
|
|
535
|
+
const routed = await resolveAutoSelection({
|
|
536
|
+
sessionId: sessionID,
|
|
537
|
+
directory,
|
|
538
|
+
model: AUTO_MODEL_REF,
|
|
539
|
+
agent: agent ?? null,
|
|
540
|
+
requestText: expandedPrompt,
|
|
541
|
+
});
|
|
542
|
+
model = { providerID: routed.model.providerID, modelID: routed.model.id };
|
|
543
|
+
variant = routed.model.variant ?? undefined;
|
|
544
|
+
agent = routed.agent || agent;
|
|
545
|
+
}
|
|
547
546
|
const parsedCommand = parseScheduledCommandPrompt(prompt);
|
|
548
547
|
let resolvedCommand = null;
|
|
549
548
|
if (parsedCommand) {
|
|
550
549
|
try {
|
|
551
|
-
const response = await client.command.list(
|
|
552
|
-
const commands =
|
|
553
|
-
|
|
554
|
-
|
|
550
|
+
const response = await client.command.list();
|
|
551
|
+
const commands = asList(response?.data);
|
|
552
|
+
if (commands.some((candidate) => candidate?.name === parsedCommand.command)) {
|
|
553
|
+
resolvedCommand = parsedCommand;
|
|
554
|
+
}
|
|
555
555
|
} catch {
|
|
556
556
|
}
|
|
557
557
|
}
|
|
558
558
|
if (goalInput.enabled) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
: null;
|
|
559
|
+
// v2 no longer publishes a command's template, so a slash command's goal
|
|
560
|
+
// objective is the prompt the user typed rather than the expanded body.
|
|
562
561
|
await (createSessionGoalOverride || createSessionGoal)({
|
|
563
562
|
baseUrl,
|
|
564
563
|
authHeaders,
|
|
565
564
|
sessionID,
|
|
566
565
|
directory,
|
|
567
|
-
objective:
|
|
566
|
+
objective: expandedPrompt,
|
|
568
567
|
tokenBudget: goalInput.tokenBudget,
|
|
569
568
|
providerID: model.providerID,
|
|
570
569
|
modelID: model.modelID,
|
|
571
570
|
onWarning: (message, error) => console.warn(`[OpenChamberSessions] ${message}:`, error?.message || error),
|
|
571
|
+
// v2 has no session-metadata route, so the goal record goes to
|
|
572
|
+
// OpenChamber's own store — the same one the proxy overlays back.
|
|
573
|
+
persistSessionGoal: async (goalSessionID, goalDirectory, goal) => {
|
|
574
|
+
await writeMetadata(goalSessionID, { openchamber: { goal } }, goalDirectory);
|
|
575
|
+
},
|
|
572
576
|
});
|
|
573
577
|
}
|
|
574
578
|
|
|
@@ -577,66 +581,75 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
577
581
|
return error;
|
|
578
582
|
};
|
|
579
583
|
|
|
584
|
+
try {
|
|
585
|
+
await applySessionSelection({ client, sessionID, model, agent, variant });
|
|
586
|
+
} catch (error) {
|
|
587
|
+
throw markGoalPartial(error);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// A session the agent dispatched has no UI to attach the project's
|
|
591
|
+
// standing context, so it is asked for here. Never fails the dispatch:
|
|
592
|
+
// a session that runs without its background beats one that never runs.
|
|
593
|
+
const knowledge = sessionKnowledgeRuntime
|
|
594
|
+
? await sessionKnowledgeRuntime.resolvePendingForSession(sessionID, directory)
|
|
595
|
+
.catch(() => ({ text: '', signature: '' }))
|
|
596
|
+
: { text: '', signature: '' };
|
|
597
|
+
// After the send is accepted, so a rejected dispatch carries it again.
|
|
598
|
+
const recordKnowledge = async () => {
|
|
599
|
+
if (knowledge.text && sessionKnowledgeRuntime) {
|
|
600
|
+
await sessionKnowledgeRuntime.recordDelivered(sessionID, directory, knowledge.signature)
|
|
601
|
+
.catch(() => undefined);
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
|
|
580
605
|
if (resolvedCommand) {
|
|
581
606
|
try {
|
|
607
|
+
// The command route takes no extra parts, so the context goes in
|
|
608
|
+
// first as a synthetic message that does not start execution.
|
|
609
|
+
if (knowledge.text) {
|
|
610
|
+
await client.session.synthetic({ sessionID, text: knowledge.text, resume: false });
|
|
611
|
+
}
|
|
582
612
|
await client.session.command({
|
|
583
613
|
sessionID,
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
...(agent ? { agent } : {}),
|
|
588
|
-
model: `${model.providerID}/${model.modelID}`,
|
|
589
|
-
...(variant ? { variant } : {}),
|
|
614
|
+
// OpenCode 2.0.8 renamed the command body field `command` to `name`.
|
|
615
|
+
name: resolvedCommand.command,
|
|
616
|
+
text: resolvedCommand.arguments || '',
|
|
590
617
|
});
|
|
591
618
|
} catch (error) {
|
|
592
619
|
throw markGoalPartial(error);
|
|
593
620
|
}
|
|
621
|
+
await recordKnowledge();
|
|
594
622
|
} else {
|
|
595
|
-
|
|
596
|
-
// A session the agent dispatched has no UI to attach the project's
|
|
597
|
-
// standing context, so it is asked for here. Never fails the dispatch:
|
|
598
|
-
// a session that runs without its background beats one that never runs.
|
|
599
|
-
const knowledge = sessionKnowledgeRuntime
|
|
600
|
-
? await sessionKnowledgeRuntime.resolvePendingForSession(sessionID, directory)
|
|
601
|
-
.catch(() => ({ text: '', signature: '' }))
|
|
602
|
-
: { text: '', signature: '' };
|
|
603
|
-
const payload = {
|
|
604
|
-
model,
|
|
605
|
-
...(agent ? { agent } : {}),
|
|
606
|
-
...(variant ? { variant } : {}),
|
|
607
|
-
parts: [
|
|
608
|
-
...(knowledge.text ? [{ type: 'text', text: knowledge.text, synthetic: true }] : []),
|
|
609
|
-
{ type: 'text', text: expandedPrompt },
|
|
610
|
-
...(goalInput.enabled
|
|
611
|
-
? [{ type: 'text', text: buildGoalIntroText(goalInput.tokenBudget), synthetic: true }]
|
|
612
|
-
: []),
|
|
613
|
-
],
|
|
614
|
-
};
|
|
615
|
-
await resolvePromptBody?.(payload, { sessionId: sessionID, directory });
|
|
623
|
+
let landedMessageID = null;
|
|
616
624
|
try {
|
|
617
|
-
|
|
625
|
+
if (knowledge.text) {
|
|
626
|
+
await client.session.synthetic({ sessionID, text: knowledge.text, resume: false });
|
|
627
|
+
}
|
|
628
|
+
const sent = await client.session.prompt({ sessionID, text: expandedPrompt });
|
|
629
|
+
landedMessageID = asNonEmptyString(sent?.id);
|
|
630
|
+
if (goalInput.enabled) {
|
|
631
|
+
// Goal mode's reminder refers to "the user message above", so it is
|
|
632
|
+
// admitted after the prompt; v2 has no way to append to one message.
|
|
633
|
+
await client.session.synthetic({
|
|
634
|
+
sessionID,
|
|
635
|
+
text: buildGoalIntroText(goalInput.tokenBudget),
|
|
636
|
+
resume: false,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
618
639
|
} catch (error) {
|
|
619
640
|
throw markGoalPartial(error);
|
|
620
641
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
}
|
|
626
|
-
const landed = await waitForPromptLanded({
|
|
627
|
-
client,
|
|
628
|
-
sessionID,
|
|
629
|
-
directory,
|
|
630
|
-
baselineUserMessageID: baseline.messageID,
|
|
631
|
-
});
|
|
632
|
-
if (!landed) {
|
|
642
|
+
await recordKnowledge();
|
|
643
|
+
if (!landedMessageID) {
|
|
644
|
+
// v2 answers a prompt with the inbox item it recorded. No item id means
|
|
645
|
+
// nothing is queued, so the dispatch must not be claimed as done.
|
|
633
646
|
return {
|
|
634
647
|
model,
|
|
635
648
|
agent,
|
|
636
649
|
variant,
|
|
637
650
|
promptDispatched: false,
|
|
638
651
|
dispatchedAsCommand: false,
|
|
639
|
-
promptError: 'OpenCode accepted the prompt but
|
|
652
|
+
promptError: 'OpenCode accepted the prompt but returned no queued message',
|
|
640
653
|
};
|
|
641
654
|
}
|
|
642
655
|
}
|
|
@@ -644,20 +657,63 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
644
657
|
return { model, agent, variant, promptDispatched: true, dispatchedAsCommand: Boolean(resolvedCommand) };
|
|
645
658
|
};
|
|
646
659
|
|
|
660
|
+
const broadcastMetadata = (sessionID, metadata) => {
|
|
661
|
+
broadcastGlobalUiEvent?.({
|
|
662
|
+
type: 'openchamber:session-metadata',
|
|
663
|
+
properties: { sessionID, metadata },
|
|
664
|
+
});
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Merge-patch a session's OpenChamber metadata on its OpenCode record: the
|
|
669
|
+
* per-session state of goal mode, session assist, obligatory context and
|
|
670
|
+
* pinned notes. The broadcast carries the full merged object, because a
|
|
671
|
+
* client that missed an earlier patch must not have to reconstruct it.
|
|
672
|
+
*/
|
|
673
|
+
const writeMetadata = async (sessionID, patch, directory = '') => {
|
|
674
|
+
if (typeof persistSessionMetadata === 'function') {
|
|
675
|
+
return persistSessionMetadata(sessionID, patch, { directory });
|
|
676
|
+
}
|
|
677
|
+
const metadata = await sessionMetadataStore.setSessionMetadata(sessionID, patch, { directory });
|
|
678
|
+
broadcastMetadata(sessionID, metadata);
|
|
679
|
+
return metadata;
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
const setMetadata = async (sessionID, payload = {}) => {
|
|
683
|
+
const id = asNonEmptyString(sessionID);
|
|
684
|
+
if (!id) throw new OpenChamberControlError('a session id is required', 400);
|
|
685
|
+
const patch = payload?.patch;
|
|
686
|
+
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
687
|
+
throw new OpenChamberControlError('patch must be an object', 400);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return { metadata: await writeMetadata(id, patch, asNonEmptyString(payload?.directory) || '') };
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
const getMetadata = async (sessionID, directory = '') => {
|
|
694
|
+
const id = asNonEmptyString(sessionID);
|
|
695
|
+
if (!id) throw new OpenChamberControlError('a session id is required', 400);
|
|
696
|
+
return { metadata: await sessionMetadataStore.get(id, { directory }) };
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const broadcastArchived = (sessionID, archivedAt) => {
|
|
700
|
+
broadcastGlobalUiEvent?.({
|
|
701
|
+
type: 'openchamber:session-archived',
|
|
702
|
+
properties: { sessionID, archivedAt },
|
|
703
|
+
});
|
|
704
|
+
};
|
|
705
|
+
|
|
647
706
|
/**
|
|
648
707
|
* Archive a batch of sessions in one request.
|
|
649
708
|
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
*
|
|
709
|
+
* OpenCode 2.x has no route that sets `time.archived`, so the state is
|
|
710
|
+
* OpenChamber's own: a JSON file beside the OpenCode instance, folded back
|
|
711
|
+
* onto the session records the proxy serves. Batching matters for the same
|
|
712
|
+
* reason it did before — the UI archives every session linked to a worktree
|
|
713
|
+
* before removing it, and doing that one request at a time is what made
|
|
714
|
+
* removing a busy worktree take tens of seconds.
|
|
655
715
|
*
|
|
656
|
-
*
|
|
657
|
-
* single OpenCode instance, and a fan-out of concurrent writes would trade a
|
|
658
|
-
* UI stall for server event-loop starvation. One failed session never stops
|
|
659
|
-
* the batch — it is reported in `failedIds` while the rest still archive, so
|
|
660
|
-
* callers keep the partial-failure behaviour they already show.
|
|
716
|
+
* No directory is needed: archive state is keyed by session id per data dir.
|
|
661
717
|
*/
|
|
662
718
|
const archive = async (payload = {}) => {
|
|
663
719
|
const parsed = parseArchiveRequest(payload);
|
|
@@ -665,41 +721,21 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
665
721
|
throw new OpenChamberControlError(parsed.error, 400);
|
|
666
722
|
}
|
|
667
723
|
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
validateDirectoryPath,
|
|
673
|
-
});
|
|
674
|
-
if (!resolvedDirectory.ok) {
|
|
675
|
-
throw new OpenChamberControlError(resolvedDirectory.error, resolvedDirectory.status || 400);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
if (typeof waitForOpenCodeReady === 'function') await waitForOpenCodeReady(10_000, 250);
|
|
679
|
-
|
|
680
|
-
const directory = resolvedDirectory.directory;
|
|
681
|
-
const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
|
|
682
|
-
const client = createOpencodeClient({ baseUrl, headers: getOpenCodeAuthHeaders() });
|
|
724
|
+
const { archived, failedIds } = await archiveStore.archive(parsed.ids, parsed.archivedAt);
|
|
725
|
+
for (const entry of archived) broadcastArchived(entry.id, entry.archivedAt);
|
|
726
|
+
return { archived, failedIds };
|
|
727
|
+
};
|
|
683
728
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
sessionID,
|
|
690
|
-
directory,
|
|
691
|
-
time: { archived: parsed.archivedAt },
|
|
692
|
-
});
|
|
693
|
-
const session = response?.data;
|
|
694
|
-
if (session?.id) archived.push(session);
|
|
695
|
-
else failedIds.push(sessionID);
|
|
696
|
-
} catch (error) {
|
|
697
|
-
console.warn('[OpenChamberSessions] failed to archive session', sessionID, error);
|
|
698
|
-
failedIds.push(sessionID);
|
|
699
|
-
}
|
|
729
|
+
/** Clears the archive flag for a batch. Mirrors `archive`. */
|
|
730
|
+
const unarchive = async (payload = {}) => {
|
|
731
|
+
const parsed = parseIdBatch(payload);
|
|
732
|
+
if (!parsed.ok) {
|
|
733
|
+
throw new OpenChamberControlError(parsed.error, 400);
|
|
700
734
|
}
|
|
701
735
|
|
|
702
|
-
|
|
736
|
+
const { restored, failedIds } = await archiveStore.unarchive(parsed.ids);
|
|
737
|
+
for (const entry of restored) broadcastArchived(entry.id, null);
|
|
738
|
+
return { restored, failedIds };
|
|
703
739
|
};
|
|
704
740
|
|
|
705
741
|
const create = async (payload = {}) => {
|
|
@@ -747,13 +783,11 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
747
783
|
await waitForWorktreeBootstrapReady({ directory: sessionDirectory });
|
|
748
784
|
}
|
|
749
785
|
|
|
750
|
-
const baseUrl =
|
|
786
|
+
const baseUrl = openCodeBaseUrl();
|
|
751
787
|
const authHeaders = getOpenCodeAuthHeaders();
|
|
752
|
-
const client =
|
|
788
|
+
const client = clientFor(sessionDirectory);
|
|
753
789
|
const sessionID = await createSession({
|
|
754
790
|
client,
|
|
755
|
-
baseUrl,
|
|
756
|
-
authHeaders,
|
|
757
791
|
directory: sessionDirectory,
|
|
758
792
|
...(title ? { title } : {}),
|
|
759
793
|
});
|
|
@@ -845,14 +879,13 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
845
879
|
requestedVariant: asNonEmptyString(payload.variant),
|
|
846
880
|
});
|
|
847
881
|
|
|
848
|
-
const baseUrl =
|
|
882
|
+
const baseUrl = openCodeBaseUrl();
|
|
849
883
|
const authHeaders = getOpenCodeAuthHeaders();
|
|
850
|
-
const client =
|
|
884
|
+
const client = clientFor(directory);
|
|
851
885
|
if (action === 'fork') {
|
|
852
886
|
targetSession = await forkSession({
|
|
853
887
|
client,
|
|
854
888
|
sessionID: sourceSessionID,
|
|
855
|
-
directory,
|
|
856
889
|
messageID: asNonEmptyString(payload.messageId) || undefined,
|
|
857
890
|
});
|
|
858
891
|
targetSessionID = targetSession.id;
|
|
@@ -861,7 +894,6 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
861
894
|
const baselineAssistantMessageId = await latestCompletedAssistantMessageID({
|
|
862
895
|
client,
|
|
863
896
|
sessionID: targetSessionID,
|
|
864
|
-
directory,
|
|
865
897
|
});
|
|
866
898
|
|
|
867
899
|
const dispatch = await dispatchPrompt({
|
|
@@ -939,6 +971,11 @@ export const createOpenChamberSessionService = (dependencies) => {
|
|
|
939
971
|
return {
|
|
940
972
|
create,
|
|
941
973
|
archive,
|
|
974
|
+
unarchive,
|
|
975
|
+
archiveStore,
|
|
976
|
+
sessionMetadataStore,
|
|
977
|
+
setMetadata,
|
|
978
|
+
getMetadata,
|
|
942
979
|
send: (sessionID, payload) => runExisting('send', sessionID, payload),
|
|
943
980
|
fork: (sessionID, payload) => runExisting('fork', sessionID, payload),
|
|
944
981
|
};
|
|
@@ -978,6 +1015,39 @@ export const registerOpenChamberSessionRoutes = (app, dependencies) => {
|
|
|
978
1015
|
}
|
|
979
1016
|
});
|
|
980
1017
|
|
|
1018
|
+
app.get('/api/openchamber/sessions/:sessionId/metadata', async (req, res) => {
|
|
1019
|
+
try {
|
|
1020
|
+
return res.json(await service.getMetadata(
|
|
1021
|
+
req.params.sessionId,
|
|
1022
|
+
asNonEmptyString(req.query?.directory) || '',
|
|
1023
|
+
));
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
console.error('[OpenChamberSessions] failed to read session metadata:', error);
|
|
1026
|
+
return sendServiceError(res, error, 'Failed to read session metadata');
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
app.post('/api/openchamber/sessions/:sessionId/metadata', express.json({ limit: '1mb' }), async (req, res) => {
|
|
1031
|
+
try {
|
|
1032
|
+
return res.json(await service.setMetadata(
|
|
1033
|
+
req.params.sessionId,
|
|
1034
|
+
req.body && typeof req.body === 'object' ? req.body : {},
|
|
1035
|
+
));
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
console.error('[OpenChamberSessions] failed to store session metadata:', error);
|
|
1038
|
+
return sendServiceError(res, error, 'Failed to store session metadata');
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
app.post('/api/openchamber/sessions/unarchive', express.json({ limit: '1mb' }), async (req, res) => {
|
|
1043
|
+
try {
|
|
1044
|
+
return res.json(await service.unarchive(req.body && typeof req.body === 'object' ? req.body : {}));
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
console.error('[OpenChamberSessions] failed to unarchive sessions:', error);
|
|
1047
|
+
return sendServiceError(res, error, 'Failed to unarchive sessions');
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
|
|
981
1051
|
app.post(
|
|
982
1052
|
'/api/openchamber/sessions/:sessionId/send',
|
|
983
1053
|
express.json({ limit: '1mb' }),
|