@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
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenChamber per-session metadata, held by OpenCode.
|
|
3
|
+
*
|
|
4
|
+
* Goal mode, session assist, obligatory context re-injection and pinned
|
|
5
|
+
* notes/plans keep their per-session state in the session's `metadata`, under
|
|
6
|
+
* the `openchamber` namespace. OpenCode 2.0.15 added `PATCH /api/session/{id}`
|
|
7
|
+
* with `metadata`, so the OpenCode record is the single authority and every
|
|
8
|
+
* client reading `session.metadata` sees the same thing.
|
|
9
|
+
*
|
|
10
|
+
* OpenCode replaces the whole object on PATCH. Writers here send a JSON Merge
|
|
11
|
+
* Patch (RFC 7386): nested objects merge key by key and a `null` deletes. The
|
|
12
|
+
* store reads the record, merges, and writes the result back, one write per
|
|
13
|
+
* session at a time, so goal mode saving progress cannot drop an assist recap
|
|
14
|
+
* that was written a moment earlier.
|
|
15
|
+
*
|
|
16
|
+
* Before 2.0.15 the state lived in `sessions-metadata.json` under the data dir.
|
|
17
|
+
* Entries still in that file are the newest metadata their sessions have: they
|
|
18
|
+
* are served from the file and pushed to OpenCode, lazily on the session's
|
|
19
|
+
* first write and in one sweep once OpenCode is up. A pushed entry leaves the
|
|
20
|
+
* file; an empty file is renamed to `sessions-metadata.json.migrated` and kept
|
|
21
|
+
* so nothing is lost if a migration turns out wrong.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import fsDefault from 'node:fs';
|
|
25
|
+
import pathDefault from 'node:path';
|
|
26
|
+
|
|
27
|
+
import { createOpenCodeClient as createOpenCodeClientDefault } from './opencode-client.js';
|
|
28
|
+
|
|
29
|
+
const LEGACY_FILE_NAME = 'sessions-metadata.json';
|
|
30
|
+
|
|
31
|
+
const asNonEmptyString = (value) => {
|
|
32
|
+
if (typeof value !== 'string') return null;
|
|
33
|
+
const trimmed = value.trim();
|
|
34
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* RFC 7386 merge. Returns a new object; `null` in the patch removes the key,
|
|
41
|
+
* and a non-object patch value replaces whatever was there.
|
|
42
|
+
*/
|
|
43
|
+
export const mergeMetadataPatch = (current, patch) => {
|
|
44
|
+
const base = isPlainObject(current) ? { ...current } : {};
|
|
45
|
+
if (!isPlainObject(patch)) return base;
|
|
46
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
47
|
+
if (value === null) {
|
|
48
|
+
delete base[key];
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
base[key] = isPlainObject(value) ? mergeMetadataPatch(base[key], value) : value;
|
|
52
|
+
}
|
|
53
|
+
return base;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const isSessionNotFound = (error) => error?._tag === 'SessionNotFoundError';
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Reads and writes one session's metadata on OpenCode. `read` resolves `null`
|
|
60
|
+
* when OpenCode does not know the session; any other failure throws, because
|
|
61
|
+
* "could not ask" must not become "empty".
|
|
62
|
+
*/
|
|
63
|
+
export const createOpenCodeSessionMetadata = ({
|
|
64
|
+
buildOpenCodeUrl,
|
|
65
|
+
getOpenCodeAuthHeaders,
|
|
66
|
+
createOpenCodeClient = createOpenCodeClientDefault,
|
|
67
|
+
}) => {
|
|
68
|
+
const clientFor = (directory) => createOpenCodeClient({
|
|
69
|
+
baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
|
|
70
|
+
headers: getOpenCodeAuthHeaders(),
|
|
71
|
+
directory,
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
read: async (sessionID, { directory = '' } = {}) => {
|
|
75
|
+
let session;
|
|
76
|
+
try {
|
|
77
|
+
// The 2.x client unwraps the `{ data }` envelope: this is the record itself.
|
|
78
|
+
session = await clientFor(directory).session.get({ sessionID });
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (isSessionNotFound(error)) return null;
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
return isPlainObject(session?.metadata) ? session.metadata : {};
|
|
84
|
+
},
|
|
85
|
+
write: (sessionID, metadata, { directory = '' } = {}) =>
|
|
86
|
+
clientFor(directory).session.update({ sessionID, metadata }),
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {object} options
|
|
92
|
+
* @param {string} options.dataDir OpenChamber data directory; holds the legacy file.
|
|
93
|
+
* @param {{ read: Function, write: Function }} options.openCode See {@link createOpenCodeSessionMetadata}.
|
|
94
|
+
* @param {typeof fsDefault.promises} [options.fsPromises]
|
|
95
|
+
* @param {typeof pathDefault} [options.path]
|
|
96
|
+
* @param {() => number} [options.now]
|
|
97
|
+
*/
|
|
98
|
+
export const createSessionMetadataStore = ({
|
|
99
|
+
dataDir,
|
|
100
|
+
openCode,
|
|
101
|
+
fsPromises = fsDefault.promises,
|
|
102
|
+
path = pathDefault,
|
|
103
|
+
now = Date.now,
|
|
104
|
+
}) => {
|
|
105
|
+
const legacyPath = path.join(dataDir, LEGACY_FILE_NAME);
|
|
106
|
+
|
|
107
|
+
/** sessionID → metadata from the legacy file that OpenCode does not hold yet. */
|
|
108
|
+
const unmigrated = new Map();
|
|
109
|
+
let legacyLoad = null;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* One chain per session: read, merge and write cannot interleave with another
|
|
113
|
+
* write to the same session. Different sessions proceed in parallel.
|
|
114
|
+
*/
|
|
115
|
+
const sessionChains = new Map();
|
|
116
|
+
const runForSession = (id, work) => {
|
|
117
|
+
const previous = sessionChains.get(id) ?? Promise.resolve();
|
|
118
|
+
const next = previous.then(work, work);
|
|
119
|
+
const settled = next.then(() => undefined, () => undefined);
|
|
120
|
+
sessionChains.set(id, settled);
|
|
121
|
+
void settled.then(() => {
|
|
122
|
+
if (sessionChains.get(id) === settled) sessionChains.delete(id);
|
|
123
|
+
});
|
|
124
|
+
return next;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** Legacy file rewrites, one at a time so an older snapshot never lands last. */
|
|
128
|
+
let fileChain = Promise.resolve();
|
|
129
|
+
const runFileWrite = (work) => {
|
|
130
|
+
const next = fileChain.then(work, work);
|
|
131
|
+
fileChain = next.then(() => undefined, () => undefined);
|
|
132
|
+
return next;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const parseLegacy = (raw) => {
|
|
136
|
+
const parsed = JSON.parse(raw);
|
|
137
|
+
if (!isPlainObject(parsed)) throw new Error('session metadata file is not a JSON object');
|
|
138
|
+
const result = new Map();
|
|
139
|
+
for (const [sessionID, value] of Object.entries(parsed)) {
|
|
140
|
+
const id = asNonEmptyString(sessionID);
|
|
141
|
+
if (id && isPlainObject(value)) result.set(id, value);
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Resolves once the legacy file has been read. A read failure rejects and
|
|
148
|
+
* lets the next call retry: writing while the file is unknown could push an
|
|
149
|
+
* older record over one the file still holds, or the file's older record over
|
|
150
|
+
* a newer write later on.
|
|
151
|
+
*/
|
|
152
|
+
const loadLegacy = () => {
|
|
153
|
+
if (!legacyLoad) {
|
|
154
|
+
legacyLoad = (async () => {
|
|
155
|
+
let raw;
|
|
156
|
+
try {
|
|
157
|
+
raw = await fsPromises.readFile(legacyPath, 'utf8');
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error?.code === 'ENOENT') return;
|
|
160
|
+
throw new Error(`session metadata is unavailable: ${error?.message ?? error}`);
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
for (const [id, metadata] of parseLegacy(raw)) unmigrated.set(id, metadata);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
// Unreadable bytes are kept for the user; there is nothing to migrate.
|
|
166
|
+
const backup = `${legacyPath}.corrupt-${now()}`;
|
|
167
|
+
await fsPromises.rename(legacyPath, backup).catch(() => undefined);
|
|
168
|
+
console.warn(`[openchamber-sessions] legacy session metadata was unreadable and was moved to ${backup}: ${error?.message ?? error}`);
|
|
169
|
+
}
|
|
170
|
+
})().catch((error) => {
|
|
171
|
+
legacyLoad = null;
|
|
172
|
+
throw error;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return legacyLoad;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/** Writes what is left to migrate, or retires the file once nothing is. */
|
|
179
|
+
const persistLegacy = () => runFileWrite(async () => {
|
|
180
|
+
if (unmigrated.size === 0) {
|
|
181
|
+
await fsPromises.rename(legacyPath, `${legacyPath}.migrated`).catch((error) => {
|
|
182
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
183
|
+
});
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const tmpPath = `${legacyPath}.${process.pid}.tmp`;
|
|
187
|
+
await fsPromises.writeFile(tmpPath, JSON.stringify(Object.fromEntries(unmigrated)), 'utf8');
|
|
188
|
+
await fsPromises.rename(tmpPath, legacyPath);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Drops a pushed entry from the legacy file. OpenCode already holds the
|
|
193
|
+
* record, so a failed rewrite does not fail the write that caused it. The
|
|
194
|
+
* entry then stays on disk and the next start pushes that older copy again;
|
|
195
|
+
* the next rewrite of the file (any other migrated session) clears it first.
|
|
196
|
+
*/
|
|
197
|
+
const forgetLegacy = async (id) => {
|
|
198
|
+
unmigrated.delete(id);
|
|
199
|
+
await persistLegacy().catch((error) => {
|
|
200
|
+
console.warn('[openchamber-sessions] could not update the legacy session metadata file:', error?.message ?? error);
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The session's full metadata: the legacy entry while it is still waiting to
|
|
206
|
+
* be migrated, OpenCode's record otherwise. `{}` for a session OpenCode does
|
|
207
|
+
* not know. Throws when OpenCode could not be asked.
|
|
208
|
+
*/
|
|
209
|
+
const get = async (sessionID, { directory = '' } = {}) => {
|
|
210
|
+
const id = asNonEmptyString(sessionID);
|
|
211
|
+
if (!id) return {};
|
|
212
|
+
await loadLegacy();
|
|
213
|
+
if (unmigrated.has(id)) return unmigrated.get(id);
|
|
214
|
+
return (await openCode.read(id, { directory })) ?? {};
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Applies a merge patch on OpenCode and returns the full metadata afterwards.
|
|
219
|
+
* Nothing is written when the current record cannot be read, so a patch never
|
|
220
|
+
* replaces fields it could not see.
|
|
221
|
+
*/
|
|
222
|
+
const setSessionMetadata = async (sessionID, patch, { directory = '' } = {}) => {
|
|
223
|
+
const id = asNonEmptyString(sessionID);
|
|
224
|
+
if (!id) throw new Error('a session id is required to store session metadata');
|
|
225
|
+
if (!isPlainObject(patch)) throw new Error('a session metadata patch must be an object');
|
|
226
|
+
await loadLegacy();
|
|
227
|
+
|
|
228
|
+
return runForSession(id, async () => {
|
|
229
|
+
const fromLegacy = unmigrated.has(id);
|
|
230
|
+
const current = fromLegacy ? unmigrated.get(id) : await openCode.read(id, { directory });
|
|
231
|
+
if (current === null) throw new Error(`session ${id} was not found`);
|
|
232
|
+
const merged = mergeMetadataPatch(current, patch);
|
|
233
|
+
await openCode.write(id, merged, { directory });
|
|
234
|
+
if (fromLegacy) await forgetLegacy(id);
|
|
235
|
+
return merged;
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Pushes every legacy entry to OpenCode. A session OpenCode no longer knows
|
|
241
|
+
* has nothing to receive its metadata, so its entry is dropped. Any other
|
|
242
|
+
* failure keeps the entry for the next sweep. Resolves the number of entries
|
|
243
|
+
* still waiting.
|
|
244
|
+
*/
|
|
245
|
+
const migrateLegacy = async () => {
|
|
246
|
+
await loadLegacy();
|
|
247
|
+
const pending = [...unmigrated.keys()];
|
|
248
|
+
if (pending.length === 0) return 0;
|
|
249
|
+
let changed = false;
|
|
250
|
+
await Promise.all(pending.map((id) => runForSession(id, async () => {
|
|
251
|
+
// A write that ran first already migrated it.
|
|
252
|
+
if (!unmigrated.has(id)) return;
|
|
253
|
+
try {
|
|
254
|
+
await openCode.write(id, unmigrated.get(id));
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (!isSessionNotFound(error)) {
|
|
257
|
+
console.warn(`[openchamber-sessions] could not migrate metadata for ${id}:`, error?.message ?? error);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
unmigrated.delete(id);
|
|
262
|
+
changed = true;
|
|
263
|
+
})));
|
|
264
|
+
if (changed) await persistLegacy();
|
|
265
|
+
return unmigrated.size;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* `{ [sessionID]: metadata }` for sessions whose metadata still lives in the
|
|
270
|
+
* legacy file. The proxy lays these over OpenCode's records until they are
|
|
271
|
+
* migrated; empty once migration is done.
|
|
272
|
+
*/
|
|
273
|
+
const listUnmigrated = async () => {
|
|
274
|
+
await loadLegacy();
|
|
275
|
+
return Object.fromEntries(unmigrated);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
get,
|
|
280
|
+
setSessionMetadata,
|
|
281
|
+
migrateLegacy,
|
|
282
|
+
listUnmigrated,
|
|
283
|
+
legacyPath,
|
|
284
|
+
};
|
|
285
|
+
};
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { createSessionMetadataStore, mergeMetadataPatch } from './session-metadata-store.js';
|
|
7
|
+
|
|
8
|
+
const tempDirs = [];
|
|
9
|
+
|
|
10
|
+
const makeDataDir = () => {
|
|
11
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-metadata-'));
|
|
12
|
+
tempDirs.push(dir);
|
|
13
|
+
return dir;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
while (tempDirs.length > 0) {
|
|
19
|
+
fs.rmSync(tempDirs.pop(), { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
vi.restoreAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('mergeMetadataPatch', () => {
|
|
25
|
+
it('merges nested objects key by key instead of replacing them', () => {
|
|
26
|
+
const current = { openchamber: { goal: { id: 'g1', status: 'active' }, assist: { recap: 'r' } } };
|
|
27
|
+
const merged = mergeMetadataPatch(current, { openchamber: { goal: { status: 'complete' } } });
|
|
28
|
+
|
|
29
|
+
expect(merged).toEqual({
|
|
30
|
+
openchamber: { goal: { id: 'g1', status: 'complete' }, assist: { recap: 'r' } },
|
|
31
|
+
});
|
|
32
|
+
// The input is untouched: callers keep whatever they already held.
|
|
33
|
+
expect(current.openchamber.goal.status).toBe('active');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('deletes a key when its patch value is null', () => {
|
|
37
|
+
expect(mergeMetadataPatch({ a: 1, b: 2 }, { b: null })).toEqual({ a: 1 });
|
|
38
|
+
expect(mergeMetadataPatch({ openchamber: { goal: {}, assist: {} } }, { openchamber: { assist: null } }))
|
|
39
|
+
.toEqual({ openchamber: { goal: {} } });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('replaces arrays and scalars rather than merging into them', () => {
|
|
43
|
+
expect(mergeMetadataPatch({ pins: ['a', 'b'] }, { pins: ['c'] })).toEqual({ pins: ['c'] });
|
|
44
|
+
expect(mergeMetadataPatch({ a: { nested: true } }, { a: 'flat' })).toEqual({ a: 'flat' });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('treats a missing or non-object base as empty', () => {
|
|
48
|
+
expect(mergeMetadataPatch(undefined, { a: 1 })).toEqual({ a: 1 });
|
|
49
|
+
expect(mergeMetadataPatch('nope', { a: 1 })).toEqual({ a: 1 });
|
|
50
|
+
expect(mergeMetadataPatch({ a: 1 }, 'nope')).toEqual({ a: 1 });
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const legacyFile = (dataDir) => path.join(dataDir, 'sessions-metadata.json');
|
|
55
|
+
const writeLegacy = (dataDir, content) => {
|
|
56
|
+
fs.writeFileSync(legacyFile(dataDir), typeof content === 'string' ? content : JSON.stringify(content));
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const notFound = () => Object.assign(new Error('Session not found'), { _tag: 'SessionNotFoundError' });
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* OpenCode's side: records by session id. A `write` replaces the whole object,
|
|
63
|
+
* which is what `PATCH /api/session/{id}` does with `metadata`.
|
|
64
|
+
*/
|
|
65
|
+
const createFakeOpenCode = (records = {}) => {
|
|
66
|
+
const sessions = new Map(Object.entries(records));
|
|
67
|
+
const fake = {
|
|
68
|
+
sessions,
|
|
69
|
+
writes: [],
|
|
70
|
+
failRead: null,
|
|
71
|
+
failWrite: null,
|
|
72
|
+
read: vi.fn(async (id) => {
|
|
73
|
+
if (fake.failRead) throw fake.failRead;
|
|
74
|
+
return sessions.has(id) ? structuredClone(sessions.get(id)) : null;
|
|
75
|
+
}),
|
|
76
|
+
write: vi.fn(async (id, metadata) => {
|
|
77
|
+
// Yield first so concurrent writers really overlap.
|
|
78
|
+
await Promise.resolve();
|
|
79
|
+
if (fake.failWrite) throw fake.failWrite;
|
|
80
|
+
if (!sessions.has(id)) throw notFound();
|
|
81
|
+
fake.writes.push([id, metadata]);
|
|
82
|
+
sessions.set(id, structuredClone(metadata));
|
|
83
|
+
}),
|
|
84
|
+
};
|
|
85
|
+
return fake;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const makeStore = (openCode, dataDir = makeDataDir()) => ({
|
|
89
|
+
dataDir,
|
|
90
|
+
store: createSessionMetadataStore({ dataDir, openCode }),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('createSessionMetadataStore', () => {
|
|
94
|
+
it('reads metadata from OpenCode, and a session OpenCode does not know as empty', async () => {
|
|
95
|
+
const openCode = createFakeOpenCode({ ses_1: { openchamber: { goal: { id: 'g1' } } } });
|
|
96
|
+
const { store } = makeStore(openCode);
|
|
97
|
+
|
|
98
|
+
await expect(store.get('ses_1')).resolves.toEqual({ openchamber: { goal: { id: 'g1' } } });
|
|
99
|
+
await expect(store.get('ses_missing')).resolves.toEqual({});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('surfaces a failed read instead of answering empty', async () => {
|
|
103
|
+
const openCode = createFakeOpenCode({ ses_1: { a: 1 } });
|
|
104
|
+
openCode.failRead = new Error('connection refused');
|
|
105
|
+
const { store } = makeStore(openCode);
|
|
106
|
+
|
|
107
|
+
await expect(store.get('ses_1')).rejects.toThrow('connection refused');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('merges a patch onto the OpenCode record and writes the whole result back', async () => {
|
|
111
|
+
const openCode = createFakeOpenCode({
|
|
112
|
+
ses_1: { openchamber: { kind: 'review', assist: { recap: 'r' } } },
|
|
113
|
+
});
|
|
114
|
+
const { store } = makeStore(openCode);
|
|
115
|
+
|
|
116
|
+
const merged = await store.setSessionMetadata('ses_1', { openchamber: { goal: { status: 'active' } } });
|
|
117
|
+
|
|
118
|
+
expect(merged).toEqual({ openchamber: { kind: 'review', assist: { recap: 'r' }, goal: { status: 'active' } } });
|
|
119
|
+
expect(openCode.sessions.get('ses_1')).toEqual(merged);
|
|
120
|
+
await store.setSessionMetadata('ses_1', { openchamber: { assist: null } });
|
|
121
|
+
expect(openCode.sessions.get('ses_1')).toEqual({ openchamber: { kind: 'review', goal: { status: 'active' } } });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('keeps both of two concurrent patches to the same session', async () => {
|
|
125
|
+
const openCode = createFakeOpenCode({ ses_1: {} });
|
|
126
|
+
const { store } = makeStore(openCode);
|
|
127
|
+
|
|
128
|
+
await Promise.all([
|
|
129
|
+
store.setSessionMetadata('ses_1', { openchamber: { goal: { status: 'active' } } }),
|
|
130
|
+
store.setSessionMetadata('ses_1', { openchamber: { assist: { recap: 'r' } } }),
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
expect(openCode.sessions.get('ses_1')).toEqual({
|
|
134
|
+
openchamber: { goal: { status: 'active' }, assist: { recap: 'r' } },
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('writes nothing when the current record cannot be read or the session is gone', async () => {
|
|
139
|
+
const openCode = createFakeOpenCode({ ses_1: { keep: true } });
|
|
140
|
+
const { store } = makeStore(openCode);
|
|
141
|
+
|
|
142
|
+
openCode.failRead = new Error('timeout');
|
|
143
|
+
await expect(store.setSessionMetadata('ses_1', { a: 1 })).rejects.toThrow('timeout');
|
|
144
|
+
openCode.failRead = null;
|
|
145
|
+
await expect(store.setSessionMetadata('ses_missing', { a: 1 })).rejects.toThrow('not found');
|
|
146
|
+
|
|
147
|
+
expect(openCode.write).not.toHaveBeenCalled();
|
|
148
|
+
expect(openCode.sessions.get('ses_1')).toEqual({ keep: true });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('rejects a missing id or a non-object patch', async () => {
|
|
152
|
+
const { store } = makeStore(createFakeOpenCode());
|
|
153
|
+
await expect(store.setSessionMetadata('', { a: 1 })).rejects.toThrow();
|
|
154
|
+
await expect(store.setSessionMetadata('ses_1', ['a'])).rejects.toThrow();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('legacy sessions-metadata.json', () => {
|
|
158
|
+
it('serves a legacy entry over the OpenCode record until it is migrated', async () => {
|
|
159
|
+
const openCode = createFakeOpenCode({ ses_1: { openchamber: { kind: 'review' } } });
|
|
160
|
+
const dataDir = makeDataDir();
|
|
161
|
+
writeLegacy(dataDir, { ses_1: { openchamber: { kind: 'review', goal: { id: 'g1' } } } });
|
|
162
|
+
const { store } = makeStore(openCode, dataDir);
|
|
163
|
+
|
|
164
|
+
await expect(store.get('ses_1')).resolves.toEqual({ openchamber: { kind: 'review', goal: { id: 'g1' } } });
|
|
165
|
+
await expect(store.listUnmigrated()).resolves.toEqual({
|
|
166
|
+
ses_1: { openchamber: { kind: 'review', goal: { id: 'g1' } } },
|
|
167
|
+
});
|
|
168
|
+
expect(openCode.read).not.toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('migrates a legacy entry on its first write, merged with the patch', async () => {
|
|
172
|
+
const openCode = createFakeOpenCode({ ses_1: { stale: true }, ses_2: {} });
|
|
173
|
+
const dataDir = makeDataDir();
|
|
174
|
+
writeLegacy(dataDir, { ses_1: { openchamber: { goal: { id: 'g1' } } }, ses_2: { notes: ['n'] } });
|
|
175
|
+
const { store } = makeStore(openCode, dataDir);
|
|
176
|
+
|
|
177
|
+
await store.setSessionMetadata('ses_1', { openchamber: { assist: { recap: 'r' } } });
|
|
178
|
+
|
|
179
|
+
// The legacy record is the newer one: it wins over what OpenCode held.
|
|
180
|
+
expect(openCode.sessions.get('ses_1')).toEqual({ openchamber: { goal: { id: 'g1' }, assist: { recap: 'r' } } });
|
|
181
|
+
expect(JSON.parse(fs.readFileSync(legacyFile(dataDir), 'utf8'))).toEqual({ ses_2: { notes: ['n'] } });
|
|
182
|
+
await expect(store.listUnmigrated()).resolves.toEqual({ ses_2: { notes: ['n'] } });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('pushes every entry, drops sessions OpenCode no longer has, and retires the file', async () => {
|
|
186
|
+
const openCode = createFakeOpenCode({ ses_1: { stale: true }, ses_2: { stale: true } });
|
|
187
|
+
const dataDir = makeDataDir();
|
|
188
|
+
// `{}` is a cleared record: it must clear OpenCode's copy too.
|
|
189
|
+
writeLegacy(dataDir, { ses_1: { openchamber: { goal: { id: 'g1' } } }, ses_2: {}, ses_gone: { a: 1 } });
|
|
190
|
+
const { store } = makeStore(openCode, dataDir);
|
|
191
|
+
|
|
192
|
+
await expect(store.migrateLegacy()).resolves.toBe(0);
|
|
193
|
+
|
|
194
|
+
expect(openCode.sessions.get('ses_1')).toEqual({ openchamber: { goal: { id: 'g1' } } });
|
|
195
|
+
expect(openCode.sessions.get('ses_2')).toEqual({});
|
|
196
|
+
expect(fs.existsSync(legacyFile(dataDir))).toBe(false);
|
|
197
|
+
expect(JSON.parse(fs.readFileSync(`${legacyFile(dataDir)}.migrated`, 'utf8'))).toMatchObject({ ses_gone: { a: 1 } });
|
|
198
|
+
await expect(store.get('ses_1')).resolves.toEqual({ openchamber: { goal: { id: 'g1' } } });
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('keeps an entry OpenCode could not take for the next sweep', async () => {
|
|
202
|
+
const openCode = createFakeOpenCode({ ses_1: {} });
|
|
203
|
+
const dataDir = makeDataDir();
|
|
204
|
+
writeLegacy(dataDir, { ses_1: { a: 1 } });
|
|
205
|
+
const { store } = makeStore(openCode, dataDir);
|
|
206
|
+
|
|
207
|
+
openCode.failWrite = new Error('connection refused');
|
|
208
|
+
await expect(store.migrateLegacy()).resolves.toBe(1);
|
|
209
|
+
expect(JSON.parse(fs.readFileSync(legacyFile(dataDir), 'utf8'))).toEqual({ ses_1: { a: 1 } });
|
|
210
|
+
await expect(store.get('ses_1')).resolves.toEqual({ a: 1 });
|
|
211
|
+
|
|
212
|
+
openCode.failWrite = null;
|
|
213
|
+
await expect(store.migrateLegacy()).resolves.toBe(0);
|
|
214
|
+
expect(openCode.sessions.get('ses_1')).toEqual({ a: 1 });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('does not push a legacy entry over a write that migrated it first', async () => {
|
|
218
|
+
const openCode = createFakeOpenCode({ ses_1: {} });
|
|
219
|
+
const dataDir = makeDataDir();
|
|
220
|
+
writeLegacy(dataDir, { ses_1: { a: 1 } });
|
|
221
|
+
const { store } = makeStore(openCode, dataDir);
|
|
222
|
+
|
|
223
|
+
await Promise.all([store.setSessionMetadata('ses_1', { b: 2 }), store.migrateLegacy()]);
|
|
224
|
+
|
|
225
|
+
expect(openCode.sessions.get('ses_1')).toEqual({ a: 1, b: 2 });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('moves a malformed file aside and has nothing to migrate', async () => {
|
|
229
|
+
const openCode = createFakeOpenCode({ ses_1: { a: 1 } });
|
|
230
|
+
const dataDir = makeDataDir();
|
|
231
|
+
writeLegacy(dataDir, '{ not json');
|
|
232
|
+
const { store } = makeStore(openCode, dataDir);
|
|
233
|
+
|
|
234
|
+
await expect(store.get('ses_1')).resolves.toEqual({ a: 1 });
|
|
235
|
+
expect(fs.readdirSync(dataDir).some((name) => name.startsWith('sessions-metadata.json.corrupt-'))).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('refuses to write while the file cannot be read, then recovers', async () => {
|
|
239
|
+
const openCode = createFakeOpenCode({ ses_1: {} });
|
|
240
|
+
const dataDir = makeDataDir();
|
|
241
|
+
const readFile = vi.fn(async () => {
|
|
242
|
+
throw Object.assign(new Error('permission denied'), { code: 'EACCES' });
|
|
243
|
+
});
|
|
244
|
+
const store = createSessionMetadataStore({
|
|
245
|
+
dataDir,
|
|
246
|
+
openCode,
|
|
247
|
+
fsPromises: { ...fs.promises, readFile },
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await expect(store.setSessionMetadata('ses_1', { a: 1 })).rejects.toThrow('permission denied');
|
|
251
|
+
expect(openCode.write).not.toHaveBeenCalled();
|
|
252
|
+
|
|
253
|
+
readFile.mockImplementation(async () => {
|
|
254
|
+
throw Object.assign(new Error('missing'), { code: 'ENOENT' });
|
|
255
|
+
});
|
|
256
|
+
await expect(store.setSessionMetadata('ses_1', { a: 1 })).resolves.toEqual({ a: 1 });
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|