@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
|
@@ -28,10 +28,6 @@ function ensureProjectSkillDir(workingDirectory) {
|
|
|
28
28
|
if (!fs.existsSync(projectSkillDir)) {
|
|
29
29
|
fs.mkdirSync(projectSkillDir, { recursive: true });
|
|
30
30
|
}
|
|
31
|
-
const legacyProjectSkillDir = path.join(workingDirectory, '.opencode', 'skill');
|
|
32
|
-
if (!fs.existsSync(legacyProjectSkillDir)) {
|
|
33
|
-
fs.mkdirSync(legacyProjectSkillDir, { recursive: true });
|
|
34
|
-
}
|
|
35
31
|
return projectSkillDir;
|
|
36
32
|
}
|
|
37
33
|
|
|
@@ -199,12 +195,18 @@ function discoverSkills(workingDirectory) {
|
|
|
199
195
|
let configuredPaths = [];
|
|
200
196
|
try {
|
|
201
197
|
const config = readConfig(workingDirectory);
|
|
202
|
-
|
|
198
|
+
// OpenCode 2 stores extra skill sources as one ordered `skills` array; v1
|
|
199
|
+
// split them into `skills.paths` and `skills.urls`. URLs are not scanned
|
|
200
|
+
// from disk either way.
|
|
201
|
+
configuredPaths = Array.isArray(config?.skills)
|
|
202
|
+
? config.skills
|
|
203
|
+
: (Array.isArray(config?.skills?.paths) ? config.skills.paths : []);
|
|
203
204
|
} catch {
|
|
204
205
|
configuredPaths = [];
|
|
205
206
|
}
|
|
206
207
|
for (const skillPath of configuredPaths) {
|
|
207
208
|
if (typeof skillPath !== 'string' || !skillPath.trim()) continue;
|
|
209
|
+
if (/^https?:\/\//i.test(skillPath.trim())) continue;
|
|
208
210
|
const expanded = skillPath.startsWith('~/')
|
|
209
211
|
? path.join(os.homedir(), skillPath.slice(2))
|
|
210
212
|
: skillPath;
|
|
@@ -32,7 +32,7 @@ export const createTunnelWiringRuntime = (dependencies) => {
|
|
|
32
32
|
setRuntimeManagedRemoteTunnelToken,
|
|
33
33
|
} = dependencies;
|
|
34
34
|
|
|
35
|
-
const initialize = (app, initialPort) => {
|
|
35
|
+
const initialize = (app, initialPort, hasUiPassword) => {
|
|
36
36
|
let activePort = initialPort;
|
|
37
37
|
|
|
38
38
|
const tunnelService = createTunnelService({
|
|
@@ -51,6 +51,7 @@ export const createTunnelWiringRuntime = (dependencies) => {
|
|
|
51
51
|
tunnelService,
|
|
52
52
|
tunnelProviderRegistry,
|
|
53
53
|
tunnelAuthController,
|
|
54
|
+
hasUiPassword,
|
|
54
55
|
readSettingsFromDiskMigrated,
|
|
55
56
|
readManagedRemoteTunnelConfigFromDisk,
|
|
56
57
|
normalizeTunnelProvider,
|
|
@@ -42,7 +42,7 @@ describe('OpenCode upgrade capability', () => {
|
|
|
42
42
|
});
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
it('
|
|
45
|
+
it('offers CLI upgrades for a managed non-bundled binary', () => {
|
|
46
46
|
expect(resolveOpenCodeUpgradeCapability({
|
|
47
47
|
isExternal: false,
|
|
48
48
|
hasManagedProcess: true,
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-arms OpenCode's own V1 -> V2 session migration for sessions that were
|
|
3
|
+
* created after it already finished.
|
|
4
|
+
*
|
|
5
|
+
* OpenCode 2.x migrates the legacy `session`/`message`/`part` tables into
|
|
6
|
+
* `session_v2`/`session_message` once and then writes `{"phase":"completed"}`
|
|
7
|
+
* under `migration.v1-v2` in `kv`. Users who kept running OpenChamber's
|
|
8
|
+
* bundled OpenCode 1.x beside a v2 install created more V1 sessions after that
|
|
9
|
+
* point, and OpenCode never looks at them again: they are simply invisible in
|
|
10
|
+
* v2. This module hands OpenCode a resume cursor before OpenChamber spawns the
|
|
11
|
+
* MANAGED OpenCode, so OpenCode's own `run()` imports them on startup with its
|
|
12
|
+
* own transform. OpenChamber never writes session rows itself.
|
|
13
|
+
*
|
|
14
|
+
* Two hard rules, both verified against v2.0.8
|
|
15
|
+
* `packages/core/src/database/v1-migration.bun.ts`:
|
|
16
|
+
*
|
|
17
|
+
* 1. NEVER delete or clear the `migration.v1-v2` row. With no row at all,
|
|
18
|
+
* OpenCode treats the database as pre-migration and DELETEs the whole
|
|
19
|
+
* `event` table — v2's durable event log — before starting over.
|
|
20
|
+
* 2. NEVER schedule a cursor that makes OpenCode revisit a session which has
|
|
21
|
+
* seen v2 activity. Every visited session gets its `session_message` rows
|
|
22
|
+
* deleted and replaced by the V1 transform, so a migrated session that was
|
|
23
|
+
* continued in v2 would lose that conversation.
|
|
24
|
+
* 3. A session deleted in v2 comes back. A v2 delete removes the `session_v2`
|
|
25
|
+
* row but leaves the legacy `session` row behind, so the loop imports it
|
|
26
|
+
* again. Decided by the maintainer (2026-09-22): people run 1.x and 2.x
|
|
27
|
+
* side by side while switching, and OpenChamber's own time-based cleanup
|
|
28
|
+
* may have deleted sessions the user never touched, so importing again is
|
|
29
|
+
* better than importing nothing. Deleting again is cheap; a lost 1.x
|
|
30
|
+
* session is not. This holds until OpenCode ships an import route that
|
|
31
|
+
* takes an explicit list of sessions.
|
|
32
|
+
*
|
|
33
|
+
* The loop OpenCode runs is `SELECT id FROM session WHERE id < cursor ORDER BY
|
|
34
|
+
* id DESC LIMIT 1`, one session at a time, so the cursor has to be strictly
|
|
35
|
+
* greater than the largest id we want imported. Session ids encode time
|
|
36
|
+
* descending in a fixed-width field that wraps, so id order is NOT time order:
|
|
37
|
+
* a 2026-08 session can sort far below a newer one. Everything here compares
|
|
38
|
+
* ids the way SQLite does (BINARY / memcmp — neither `session.id` nor
|
|
39
|
+
* `session_v2.id` declares a collation) and never assumes id order means age.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import fs from 'node:fs';
|
|
43
|
+
import path from 'node:path';
|
|
44
|
+
import { createRequire } from 'node:module';
|
|
45
|
+
import { z } from 'zod';
|
|
46
|
+
|
|
47
|
+
import { resolveCredentialDbPath } from './credential-db.js';
|
|
48
|
+
import { OPENCODE_DATA_DIR } from './auth.js';
|
|
49
|
+
|
|
50
|
+
/** OpenCode's key for the migration state row in `kv`. */
|
|
51
|
+
const MIGRATION_STATE_KEY = 'migration.v1-v2';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The `session_message.type` values OpenCode's V1 transform can produce
|
|
55
|
+
* (`transformSession` emits only these four). Any other type on a session is
|
|
56
|
+
* proof the session was used in v2 after it was migrated.
|
|
57
|
+
*/
|
|
58
|
+
const V1_IMPORT_MESSAGE_TYPES = ['user', 'assistant', 'synthetic', 'compaction'];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Appended to the largest missing id to build the cursor. SQLite compares TEXT
|
|
62
|
+
* byte by byte, so `<id><anything>` sorts immediately after `<id>`, and since
|
|
63
|
+
* every session id has the same length no real id can fall between the two.
|
|
64
|
+
* The cursor therefore selects exactly "this id and everything below it".
|
|
65
|
+
*/
|
|
66
|
+
const CURSOR_SUFFIX = '';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The smallest cursor that still makes OpenCode's `id < cursor` loop visit
|
|
70
|
+
* `maxMissingId` itself.
|
|
71
|
+
* @param {string} maxMissingId
|
|
72
|
+
*/
|
|
73
|
+
export const computeResumeCursor = (maxMissingId) => `${maxMissingId}${CURSOR_SUFFIX}`;
|
|
74
|
+
|
|
75
|
+
const kvRowSchema = z.object({ value: z.string(), time_updated: z.number() });
|
|
76
|
+
const idRowSchema = z.object({ id: z.string() });
|
|
77
|
+
const nameRowSchema = z.object({ name: z.string() });
|
|
78
|
+
const countRowSchema = z.object({ value: z.number() });
|
|
79
|
+
const nullableIdRowSchema = z.object({ id: z.string().nullable() });
|
|
80
|
+
const migrationStateSchema = z.object({ phase: z.string() });
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A minimal read-write connection: `all(sql, params)`, `run(sql, params)` and
|
|
84
|
+
* `close()`. Node provides it through `node:sqlite`, Bun through `bun:sqlite`;
|
|
85
|
+
* both are built in and no dependency is added. Neither being available
|
|
86
|
+
* yields null and the top-up is skipped.
|
|
87
|
+
*/
|
|
88
|
+
let openConnection;
|
|
89
|
+
const loadSqlite = () => {
|
|
90
|
+
if (openConnection !== undefined) return openConnection;
|
|
91
|
+
const require = createRequire(import.meta.url);
|
|
92
|
+
openConnection = null;
|
|
93
|
+
try {
|
|
94
|
+
const { DatabaseSync } = process.getBuiltinModule?.('node:sqlite') ?? require('node:sqlite');
|
|
95
|
+
// Opening an in-memory database proves the builtin is usable before we
|
|
96
|
+
// commit to it; a Node without `node:sqlite` throws here and Bun is tried.
|
|
97
|
+
new DatabaseSync(':memory:').close();
|
|
98
|
+
openConnection = (dbPath) => {
|
|
99
|
+
const db = new DatabaseSync(dbPath);
|
|
100
|
+
return {
|
|
101
|
+
all: (sql, params = []) => db.prepare(sql).all(...params),
|
|
102
|
+
run: (sql, params = []) => {
|
|
103
|
+
db.prepare(sql).run(...params);
|
|
104
|
+
},
|
|
105
|
+
close: () => db.close(),
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
return openConnection;
|
|
109
|
+
} catch {
|
|
110
|
+
// Not Node, or a Node without node:sqlite; try Bun next.
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const { Database } = require('bun:sqlite');
|
|
114
|
+
new Database(':memory:').close();
|
|
115
|
+
openConnection = (dbPath) => {
|
|
116
|
+
const db = new Database(dbPath);
|
|
117
|
+
return {
|
|
118
|
+
all: (sql, params = []) => db.query(sql).all(...params),
|
|
119
|
+
run: (sql, params = []) => {
|
|
120
|
+
db.query(sql).run(...params);
|
|
121
|
+
},
|
|
122
|
+
close: () => db.close(),
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
} catch {
|
|
126
|
+
// No sqlite runtime at all.
|
|
127
|
+
}
|
|
128
|
+
return openConnection;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const outcome = (status, reason, missing = 0, revisited = 0) =>
|
|
132
|
+
reason === undefined ? { status, missing, revisited } : { status, missing, revisited, reason };
|
|
133
|
+
|
|
134
|
+
const firstRow = (schema, rows) => {
|
|
135
|
+
const parsed = schema.safeParse(rows[0]);
|
|
136
|
+
return parsed.success ? parsed.data : null;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const hasTables = (db, names) => {
|
|
140
|
+
const rows = db.all(
|
|
141
|
+
`SELECT name FROM sqlite_master WHERE type = 'table' AND name IN (${names.map(() => '?').join(', ')})`,
|
|
142
|
+
names,
|
|
143
|
+
);
|
|
144
|
+
const found = new Set(
|
|
145
|
+
rows.flatMap((row) => {
|
|
146
|
+
const parsed = nameRowSchema.safeParse(row);
|
|
147
|
+
return parsed.success ? [parsed.data.name] : [];
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
return names.every((name) => found.has(name));
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The completion timestamp of a `{"phase":"completed"}` migration row, or null
|
|
155
|
+
* when the row is missing, unreadable, or the migration is not finished.
|
|
156
|
+
*/
|
|
157
|
+
const readCompletedMigration = (db) => {
|
|
158
|
+
const row = firstRow(kvRowSchema, db.all('SELECT value, time_updated FROM kv WHERE key = ?', [MIGRATION_STATE_KEY]));
|
|
159
|
+
if (!row) return null;
|
|
160
|
+
let decoded;
|
|
161
|
+
try {
|
|
162
|
+
decoded = JSON.parse(row.value);
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const state = migrationStateSchema.safeParse(decoded);
|
|
167
|
+
if (!state.success || state.data.phase !== 'completed') return null;
|
|
168
|
+
return { completedAt: row.time_updated };
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Sessions OpenCode will revisit at this cursor that carry evidence of v2 use
|
|
173
|
+
* after the migration finished: a message created after completion, a session
|
|
174
|
+
* row touched after completion, or a message type the V1 transform never
|
|
175
|
+
* emits. Any hit means the top-up must not run.
|
|
176
|
+
*/
|
|
177
|
+
const findRevisitedSessionsWithV2Activity = (db, cursor, completedAt) =>
|
|
178
|
+
db
|
|
179
|
+
.all(
|
|
180
|
+
`SELECT s.id AS id FROM session s
|
|
181
|
+
WHERE s.id < ?
|
|
182
|
+
AND s.id IN (SELECT id FROM session_v2)
|
|
183
|
+
AND (
|
|
184
|
+
EXISTS (SELECT 1 FROM session_v2 v WHERE v.id = s.id AND v.time_updated > ?)
|
|
185
|
+
OR EXISTS (
|
|
186
|
+
SELECT 1 FROM session_message m
|
|
187
|
+
WHERE m.session_id = s.id
|
|
188
|
+
AND (m.time_created > ? OR m.type NOT IN (${V1_IMPORT_MESSAGE_TYPES.map(() => '?').join(', ')}))
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
ORDER BY s.id DESC`,
|
|
192
|
+
[cursor, completedAt, completedAt, ...V1_IMPORT_MESSAGE_TYPES],
|
|
193
|
+
)
|
|
194
|
+
.flatMap((row) => {
|
|
195
|
+
const parsed = idRowSchema.safeParse(row);
|
|
196
|
+
return parsed.success ? [parsed.data.id] : [];
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
/** A legacy session with no `session_v2` twin, whether never imported or deleted in v2 (see rule 3). */
|
|
200
|
+
const MISSING_CONDITION = 'id NOT IN (SELECT id FROM session_v2)';
|
|
201
|
+
|
|
202
|
+
const countMissingSessions = (db) =>
|
|
203
|
+
firstRow(countRowSchema, db.all(`SELECT COUNT(*) AS value FROM session WHERE ${MISSING_CONDITION}`))?.value ?? 0;
|
|
204
|
+
|
|
205
|
+
const countRevisitedSessions = (db, cursor) =>
|
|
206
|
+
firstRow(
|
|
207
|
+
countRowSchema,
|
|
208
|
+
db.all('SELECT COUNT(*) AS value FROM session WHERE id < ? AND id IN (SELECT id FROM session_v2)', [cursor]),
|
|
209
|
+
)?.value ?? 0;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Schedule OpenCode's own V1 migration to pick up the V1 sessions that are
|
|
213
|
+
* missing from v2, when that can be done without touching a session that has
|
|
214
|
+
* v2 activity.
|
|
215
|
+
*
|
|
216
|
+
* Call this only for a MANAGED OpenCode, before it is spawned: writing the
|
|
217
|
+
* migration state under a running OpenCode would race its own loop, and an
|
|
218
|
+
* external OpenCode is not OpenChamber's to steer.
|
|
219
|
+
*
|
|
220
|
+
* @param {{ dbPath?: string, fileSystem?: typeof fs, logger?: Pick<Console, 'log' | 'warn'>, now?: () => number }} [options]
|
|
221
|
+
* `unsafe` names the one refusal, `revisited-sessions-have-v2-activity`: an
|
|
222
|
+
* already-migrated session under the cursor was used in v2 since.
|
|
223
|
+
*
|
|
224
|
+
* @returns {{ status: 'skipped' | 'scheduled' | 'unsafe' | 'unavailable', missing: number, revisited: number, reason?: string }}
|
|
225
|
+
*/
|
|
226
|
+
export const topUpV1Migration = (options = {}) => {
|
|
227
|
+
const {
|
|
228
|
+
dbPath = resolveCredentialDbPath({ dataDir: OPENCODE_DATA_DIR, path }),
|
|
229
|
+
fileSystem = fs,
|
|
230
|
+
logger = console,
|
|
231
|
+
now = Date.now,
|
|
232
|
+
} = options;
|
|
233
|
+
|
|
234
|
+
const open = loadSqlite();
|
|
235
|
+
if (!open) return outcome('unavailable', 'no-sqlite-runtime');
|
|
236
|
+
if (!fileSystem.existsSync(dbPath)) return outcome('skipped', 'no-database');
|
|
237
|
+
|
|
238
|
+
let db;
|
|
239
|
+
try {
|
|
240
|
+
db = open(dbPath);
|
|
241
|
+
if (!hasTables(db, ['session', 'session_v2', 'session_message', 'kv'])) {
|
|
242
|
+
return outcome('skipped', 'no-v1-sessions');
|
|
243
|
+
}
|
|
244
|
+
const migration = readCompletedMigration(db);
|
|
245
|
+
if (!migration) return outcome('skipped', 'migration-not-completed');
|
|
246
|
+
|
|
247
|
+
const missingCount = countMissingSessions(db);
|
|
248
|
+
if (missingCount === 0) return outcome('skipped', 'nothing-missing');
|
|
249
|
+
|
|
250
|
+
const maxMissing = firstRow(
|
|
251
|
+
nullableIdRowSchema,
|
|
252
|
+
db.all(`SELECT MAX(id) AS id FROM session WHERE ${MISSING_CONDITION}`),
|
|
253
|
+
)?.id;
|
|
254
|
+
if (!maxMissing) return outcome('skipped', 'nothing-missing');
|
|
255
|
+
|
|
256
|
+
const cursor = computeResumeCursor(maxMissing);
|
|
257
|
+
const revisitedCount = countRevisitedSessions(db, cursor);
|
|
258
|
+
const unsafe = findRevisitedSessionsWithV2Activity(db, cursor, migration.completedAt);
|
|
259
|
+
if (unsafe.length > 0) {
|
|
260
|
+
logger.warn(
|
|
261
|
+
`[OpenCode] ${missingCount} OpenCode 1.x session(s) are missing from the v2 database, but importing them ` +
|
|
262
|
+
`would make OpenCode rewrite ${unsafe.length} already-migrated session(s) that have been used in v2 ` +
|
|
263
|
+
`since. Leaving the migration state untouched.`,
|
|
264
|
+
);
|
|
265
|
+
return outcome('unsafe', 'revisited-sessions-have-v2-activity', missingCount, revisitedCount);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
db.run('UPDATE kv SET value = ?, time_updated = ? WHERE key = ?', [
|
|
269
|
+
JSON.stringify({ phase: 'sessions', cursor }),
|
|
270
|
+
now(),
|
|
271
|
+
MIGRATION_STATE_KEY,
|
|
272
|
+
]);
|
|
273
|
+
logger.log(
|
|
274
|
+
`[OpenCode] Scheduled the import of ${missingCount} OpenCode 1.x session(s) created after the v2 migration; ` +
|
|
275
|
+
`OpenCode will re-import ${revisitedCount} untouched session(s) on the way.`,
|
|
276
|
+
);
|
|
277
|
+
return outcome('scheduled', undefined, missingCount, revisitedCount);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
logger.warn('[OpenCode] Could not check the v1 session migration:', error instanceof Error ? error.message : error);
|
|
280
|
+
return outcome('unavailable', 'database-error');
|
|
281
|
+
} finally {
|
|
282
|
+
try {
|
|
283
|
+
db?.close();
|
|
284
|
+
} catch {
|
|
285
|
+
// Closing a database we are done with must never break startup.
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
};
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
|
|
7
|
+
import { computeResumeCursor, topUpV1Migration } from './v1-migration-topup.js';
|
|
8
|
+
|
|
9
|
+
const sqlite = (() => {
|
|
10
|
+
try {
|
|
11
|
+
return createRequire(import.meta.url)('node:sqlite');
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
})();
|
|
16
|
+
|
|
17
|
+
const MIGRATION_KEY = 'migration.v1-v2';
|
|
18
|
+
/** Stands in for "when OpenCode finished the migration". */
|
|
19
|
+
const COMPLETED_AT = 1_000_000;
|
|
20
|
+
|
|
21
|
+
let dir;
|
|
22
|
+
let dbPath;
|
|
23
|
+
|
|
24
|
+
const openDb = () => new sqlite.DatabaseSync(dbPath);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build a database with the tables the top-up reads, in OpenCode's shape.
|
|
28
|
+
* `options.tables` drops tables to cover half-built databases.
|
|
29
|
+
*/
|
|
30
|
+
const createDatabase = ({ tables = ['session', 'session_v2', 'session_message', 'kv', 'event'] } = {}) => {
|
|
31
|
+
const db = openDb();
|
|
32
|
+
if (tables.includes('session')) {
|
|
33
|
+
db.exec('CREATE TABLE session (id TEXT PRIMARY KEY, project_id TEXT, time_created INTEGER, time_updated INTEGER)');
|
|
34
|
+
}
|
|
35
|
+
if (tables.includes('session_v2')) {
|
|
36
|
+
db.exec('CREATE TABLE session_v2 (id TEXT PRIMARY KEY, time_created INTEGER, time_updated INTEGER)');
|
|
37
|
+
}
|
|
38
|
+
if (tables.includes('session_message')) {
|
|
39
|
+
db.exec(
|
|
40
|
+
'CREATE TABLE session_message (id TEXT PRIMARY KEY, session_id TEXT, type TEXT, seq INTEGER, time_created INTEGER, time_updated INTEGER)',
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if (tables.includes('kv')) {
|
|
44
|
+
db.exec('CREATE TABLE kv (key TEXT PRIMARY KEY, value TEXT NOT NULL, time_created INTEGER, time_updated INTEGER)');
|
|
45
|
+
}
|
|
46
|
+
if (tables.includes('event')) {
|
|
47
|
+
db.exec('CREATE TABLE event (id TEXT PRIMARY KEY, payload TEXT)');
|
|
48
|
+
}
|
|
49
|
+
db.close();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `v1` entries are ids or `{ id, timeCreated }`. A bare id stands for a session
|
|
54
|
+
* created after the migration completed (the case the top-up exists for); a
|
|
55
|
+
* session the migration already walked needs `timeCreated` before COMPLETED_AT.
|
|
56
|
+
*/
|
|
57
|
+
const seed = ({ v1 = [], v2 = [], messages = [], migration = { phase: 'completed' } } = {}) => {
|
|
58
|
+
const db = openDb();
|
|
59
|
+
for (const entry of v1) {
|
|
60
|
+
const { id, timeCreated = COMPLETED_AT + 1 } = typeof entry === 'string' ? { id: entry } : entry;
|
|
61
|
+
db.prepare('INSERT INTO session (id, project_id, time_created, time_updated) VALUES (?, ?, ?, ?)').run(
|
|
62
|
+
id,
|
|
63
|
+
'prj',
|
|
64
|
+
timeCreated,
|
|
65
|
+
timeCreated,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
for (const session of v2) {
|
|
69
|
+
db.prepare('INSERT INTO session_v2 (id, time_created, time_updated) VALUES (?, ?, ?)').run(
|
|
70
|
+
session.id,
|
|
71
|
+
1,
|
|
72
|
+
session.timeUpdated ?? COMPLETED_AT - 500,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
for (const [index, message] of messages.entries()) {
|
|
76
|
+
db.prepare(
|
|
77
|
+
'INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated) VALUES (?, ?, ?, ?, ?, ?)',
|
|
78
|
+
).run(`msg_${index}`, message.session, message.type, index, message.timeCreated ?? COMPLETED_AT - 500, 1);
|
|
79
|
+
}
|
|
80
|
+
if (migration) {
|
|
81
|
+
db.prepare('INSERT INTO kv (key, value, time_created, time_updated) VALUES (?, ?, ?, ?)').run(
|
|
82
|
+
MIGRATION_KEY,
|
|
83
|
+
JSON.stringify(migration),
|
|
84
|
+
1,
|
|
85
|
+
COMPLETED_AT,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
db.close();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const readMigrationRow = () => {
|
|
92
|
+
const db = openDb();
|
|
93
|
+
const row = db.prepare('SELECT value, time_updated FROM kv WHERE key = ?').all(MIGRATION_KEY)[0];
|
|
94
|
+
db.close();
|
|
95
|
+
return row ? { state: JSON.parse(row.value), timeUpdated: row.time_updated } : null;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const countEvents = () => {
|
|
99
|
+
const db = openDb();
|
|
100
|
+
const count = db.prepare('SELECT COUNT(*) AS value FROM event').all()[0].value;
|
|
101
|
+
db.close();
|
|
102
|
+
return count;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const run = () => topUpV1Migration({ dbPath, logger: { log: () => {}, warn: () => {} }, now: () => 2_000_000 });
|
|
106
|
+
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'oc-topup-'));
|
|
109
|
+
dbPath = path.join(dir, 'opencode.db');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
afterEach(() => {
|
|
113
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('computeResumeCursor', () => {
|
|
117
|
+
it('sorts immediately above the id it must reach', () => {
|
|
118
|
+
const cursor = computeResumeCursor('ses_b');
|
|
119
|
+
expect(cursor > 'ses_b').toBe(true);
|
|
120
|
+
// No same-length id can fall between the id and the cursor.
|
|
121
|
+
expect(cursor > 'ses_c').toBe(false);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe.skipIf(!sqlite)('topUpV1Migration', () => {
|
|
126
|
+
it('skips when the database file does not exist', () => {
|
|
127
|
+
expect(run()).toEqual({ status: 'skipped', missing: 0, revisited: 0, reason: 'no-database' });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('skips a database without the v1 session table', () => {
|
|
131
|
+
createDatabase({ tables: ['session_v2', 'session_message', 'kv', 'event'] });
|
|
132
|
+
seed({ v1: [] });
|
|
133
|
+
expect(run()).toEqual({ status: 'skipped', missing: 0, revisited: 0, reason: 'no-v1-sessions' });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('skips when the migration row is absent, and never clears it', () => {
|
|
137
|
+
createDatabase();
|
|
138
|
+
seed({ v1: ['ses_a'], migration: null });
|
|
139
|
+
expect(run()).toEqual({ status: 'skipped', missing: 0, revisited: 0, reason: 'migration-not-completed' });
|
|
140
|
+
expect(readMigrationRow()).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('skips while OpenCode is still migrating', () => {
|
|
144
|
+
createDatabase();
|
|
145
|
+
seed({ v1: ['ses_a'], migration: { phase: 'sessions', cursor: 'ses_b' } });
|
|
146
|
+
expect(run()).toEqual({ status: 'skipped', missing: 0, revisited: 0, reason: 'migration-not-completed' });
|
|
147
|
+
expect(readMigrationRow().state).toEqual({ phase: 'sessions', cursor: 'ses_b' });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('does nothing when every v1 session is already in v2', () => {
|
|
151
|
+
createDatabase();
|
|
152
|
+
seed({ v1: ['ses_a', 'ses_b'], v2: [{ id: 'ses_a' }, { id: 'ses_b' }] });
|
|
153
|
+
expect(run()).toEqual({ status: 'skipped', missing: 0, revisited: 0, reason: 'nothing-missing' });
|
|
154
|
+
expect(readMigrationRow().state).toEqual({ phase: 'completed' });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('schedules a cursor for missing sessions when nothing would be revisited', () => {
|
|
158
|
+
createDatabase();
|
|
159
|
+
seed({ v1: ['ses_c', 'ses_d'], v2: [] });
|
|
160
|
+
expect(run()).toEqual({ status: 'scheduled', missing: 2, revisited: 0 });
|
|
161
|
+
expect(readMigrationRow()).toEqual({
|
|
162
|
+
state: { phase: 'sessions', cursor: computeResumeCursor('ses_d') },
|
|
163
|
+
timeUpdated: 2_000_000,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('schedules when the sessions it will revisit were never touched in v2', () => {
|
|
168
|
+
createDatabase();
|
|
169
|
+
seed({
|
|
170
|
+
v1: ['ses_a', 'ses_d'],
|
|
171
|
+
v2: [{ id: 'ses_a' }],
|
|
172
|
+
messages: [{ session: 'ses_a', type: 'user' }, { session: 'ses_a', type: 'assistant' }],
|
|
173
|
+
});
|
|
174
|
+
expect(run()).toEqual({ status: 'scheduled', missing: 1, revisited: 1 });
|
|
175
|
+
expect(readMigrationRow().state).toEqual({ phase: 'sessions', cursor: computeResumeCursor('ses_d') });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('refuses when a revisited session gained a message after the migration finished', () => {
|
|
179
|
+
createDatabase();
|
|
180
|
+
seed({
|
|
181
|
+
v1: ['ses_a', 'ses_d'],
|
|
182
|
+
v2: [{ id: 'ses_a' }],
|
|
183
|
+
messages: [{ session: 'ses_a', type: 'user', timeCreated: COMPLETED_AT + 1 }],
|
|
184
|
+
});
|
|
185
|
+
expect(run()).toEqual({
|
|
186
|
+
status: 'unsafe',
|
|
187
|
+
missing: 1,
|
|
188
|
+
revisited: 1,
|
|
189
|
+
reason: 'revisited-sessions-have-v2-activity',
|
|
190
|
+
});
|
|
191
|
+
expect(readMigrationRow().state).toEqual({ phase: 'completed' });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('refuses when a revisited session carries a message type the v1 import never produces', () => {
|
|
195
|
+
createDatabase();
|
|
196
|
+
seed({
|
|
197
|
+
v1: ['ses_a', 'ses_d'],
|
|
198
|
+
v2: [{ id: 'ses_a' }],
|
|
199
|
+
messages: [{ session: 'ses_a', type: 'shell' }],
|
|
200
|
+
});
|
|
201
|
+
expect(run().status).toBe('unsafe');
|
|
202
|
+
expect(readMigrationRow().state).toEqual({ phase: 'completed' });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('refuses when a revisited session row was updated after the migration finished', () => {
|
|
206
|
+
createDatabase();
|
|
207
|
+
seed({ v1: ['ses_a', 'ses_d'], v2: [{ id: 'ses_a', timeUpdated: COMPLETED_AT + 10 }] });
|
|
208
|
+
expect(run().status).toBe('unsafe');
|
|
209
|
+
expect(readMigrationRow().state).toEqual({ phase: 'completed' });
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('handles wrapped session ids: the cursor comes from the largest missing id', () => {
|
|
213
|
+
// Real ids wrap, so a 2026-08 session (`ses_002f…`) sorts far below a
|
|
214
|
+
// newer one (`ses_f4b7…`). The migrated low session is below the cursor
|
|
215
|
+
// and therefore counted as revisited.
|
|
216
|
+
createDatabase();
|
|
217
|
+
seed({
|
|
218
|
+
v1: ['ses_002fb287fffe', 'ses_f4b70000aaaa'],
|
|
219
|
+
v2: [{ id: 'ses_002fb287fffe' }],
|
|
220
|
+
messages: [{ session: 'ses_002fb287fffe', type: 'user' }],
|
|
221
|
+
});
|
|
222
|
+
expect(run()).toEqual({ status: 'scheduled', missing: 1, revisited: 1 });
|
|
223
|
+
expect(readMigrationRow().state).toEqual({
|
|
224
|
+
phase: 'sessions',
|
|
225
|
+
cursor: computeResumeCursor('ses_f4b70000aaaa'),
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('imports a session deleted in v2 again rather than leaving newer 1.x sessions behind', () => {
|
|
230
|
+
// ses_a was walked by the migration and deleted in v2 (only the legacy row
|
|
231
|
+
// is left); ses_d was created after. The maintainer chose to bring ses_a
|
|
232
|
+
// back over losing ses_d: deleting again is cheap, a lost session is not.
|
|
233
|
+
createDatabase();
|
|
234
|
+
seed({ v1: [{ id: 'ses_a', timeCreated: COMPLETED_AT - 1 }, 'ses_d'] });
|
|
235
|
+
expect(run()).toEqual({ status: 'scheduled', missing: 2, revisited: 0 });
|
|
236
|
+
expect(readMigrationRow().state).toEqual({ phase: 'sessions', cursor: computeResumeCursor('ses_d') });
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('never touches the durable event log', () => {
|
|
240
|
+
createDatabase();
|
|
241
|
+
const db = openDb();
|
|
242
|
+
db.prepare('INSERT INTO event (id, payload) VALUES (?, ?)').run('evt_1', '{}');
|
|
243
|
+
db.close();
|
|
244
|
+
seed({ v1: ['ses_d'] });
|
|
245
|
+
expect(run().status).toBe('scheduled');
|
|
246
|
+
expect(countEvents()).toBe(1);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('skips on a second run, once OpenCode has imported the sessions', () => {
|
|
250
|
+
createDatabase();
|
|
251
|
+
seed({ v1: ['ses_d'] });
|
|
252
|
+
expect(run().status).toBe('scheduled');
|
|
253
|
+
|
|
254
|
+
// OpenCode runs, imports ses_d and marks the migration completed again.
|
|
255
|
+
const db = openDb();
|
|
256
|
+
db.prepare('INSERT INTO session_v2 (id, time_created, time_updated) VALUES (?, ?, ?)').run('ses_d', 1, 2_000_100);
|
|
257
|
+
db.prepare('UPDATE kv SET value = ?, time_updated = ? WHERE key = ?').run(
|
|
258
|
+
JSON.stringify({ phase: 'completed' }),
|
|
259
|
+
2_000_200,
|
|
260
|
+
MIGRATION_KEY,
|
|
261
|
+
);
|
|
262
|
+
db.close();
|
|
263
|
+
|
|
264
|
+
expect(run()).toEqual({ status: 'skipped', missing: 0, revisited: 0, reason: 'nothing-missing' });
|
|
265
|
+
});
|
|
266
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function supportsOpenCodeV2Install(platform?: NodeJS.Platform): boolean;
|
|
2
|
+
export function installOpenCodeV2(options?: {
|
|
3
|
+
homeDirectory?: string;
|
|
4
|
+
env?: NodeJS.ProcessEnv;
|
|
5
|
+
fetchImpl?: typeof fetch;
|
|
6
|
+
platform?: NodeJS.Platform;
|
|
7
|
+
tarCommand?: string;
|
|
8
|
+
}): Promise<string>;
|