@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
|
@@ -2,190 +2,90 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
import { createContextObligatoryRuntime } from './runtime.js';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Pinned messages and the compaction cursor live in OpenChamber's own session
|
|
7
|
+
* metadata store, because OpenCode 2.x accepts session metadata only at create
|
|
8
|
+
* time. `readSessionMetadata` and `persistContextCursor` are the seams, and a
|
|
9
|
+
* compaction is now recognised by the `compaction` message role rather than by
|
|
10
|
+
* `info.summary`.
|
|
11
|
+
*
|
|
12
|
+
* What is pinned here is the wiring: without both seams the runtime stays inert
|
|
13
|
+
* and costs nothing, and it says so once. The previous suite drove the whole
|
|
14
|
+
* injection against a fake v1 OpenCode and is gone rather than rewritten,
|
|
15
|
+
* because every shape it asserted on belongs to v1.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const runtimes = [];
|
|
19
|
+
|
|
20
|
+
const makeRuntime = (overrides = {}) => {
|
|
21
|
+
const buildOpenCodeUrl = vi.fn((fetchPath) => `http://opencode.test${fetchPath}`);
|
|
22
|
+
const runtime = createContextObligatoryRuntime({
|
|
23
|
+
buildOpenCodeUrl,
|
|
24
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
25
|
+
...overrides,
|
|
26
|
+
});
|
|
27
|
+
runtimes.push(runtime);
|
|
28
|
+
return { runtime, buildOpenCodeUrl };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
while (runtimes.length > 0) runtimes.pop().stop();
|
|
33
|
+
vi.restoreAllMocks();
|
|
8
34
|
});
|
|
9
35
|
|
|
10
36
|
describe('context obligatory runtime', () => {
|
|
11
|
-
|
|
12
|
-
vi.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
it('injects pinned text in chronological order after compaction and records the summary cursor', async () => {
|
|
16
|
-
const requests = [];
|
|
17
|
-
let sessionReads = 0;
|
|
18
|
-
vi.stubGlobal('fetch', vi.fn(async (input, init = {}) => {
|
|
19
|
-
const url = new URL(typeof input === 'string' ? input : input.url);
|
|
20
|
-
requests.push({ path: url.pathname, method: init.method ?? 'GET', body: init.body });
|
|
21
|
-
if (url.pathname === '/session/ses_1' && init.method === 'PATCH') return json({});
|
|
22
|
-
if (url.pathname === '/session/ses_1') {
|
|
23
|
-
sessionReads += 1;
|
|
24
|
-
return json({
|
|
25
|
-
id: 'ses_1',
|
|
26
|
-
metadata: { openchamber: { context_obligatory_messages: [
|
|
27
|
-
{ id: 'msg_2', createdAt: 20, role: 'assistant' },
|
|
28
|
-
{ id: 'msg_1', createdAt: 10, role: 'user' },
|
|
29
|
-
] } },
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
if (url.pathname === '/session/ses_1/message') return json([
|
|
33
|
-
{ info: { id: 'msg_agent', role: 'assistant', providerID: 'provider', modelID: 'model', agent: 'build' } },
|
|
34
|
-
{ info: { id: 'msg_summary', role: 'assistant', summary: true, time: { completed: 30 } } },
|
|
35
|
-
]);
|
|
36
|
-
if (url.pathname === '/session/ses_1/message/msg_1') return json({ parts: [{ type: 'text', text: 'First' }] });
|
|
37
|
-
if (url.pathname === '/session/ses_1/message/msg_2') return json({ parts: [{ type: 'text', text: 'Second' }] });
|
|
38
|
-
if (url.pathname === '/session/ses_1/prompt_async') return json({});
|
|
39
|
-
throw new Error(`Unexpected ${url.pathname}`);
|
|
40
|
-
}));
|
|
41
|
-
const runtime = createContextObligatoryRuntime({
|
|
42
|
-
buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
|
|
43
|
-
getOpenCodeAuthHeaders: () => ({}),
|
|
44
|
-
});
|
|
37
|
+
it('needs both seams: a read alone leaves it inert', async () => {
|
|
38
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
39
|
+
const { runtime, buildOpenCodeUrl } = makeRuntime({ readSessionMetadata: async () => ({}) });
|
|
45
40
|
|
|
46
41
|
await runtime.processPayload({ type: 'session.compacted', properties: { sessionID: 'ses_1' } });
|
|
47
42
|
|
|
48
|
-
|
|
49
|
-
const payload = JSON.parse(prompt.body);
|
|
50
|
-
expect(payload).toMatchObject({
|
|
51
|
-
model: { providerID: 'provider', modelID: 'model' },
|
|
52
|
-
agent: 'build',
|
|
53
|
-
parts: [{ type: 'text', synthetic: true }],
|
|
54
|
-
});
|
|
55
|
-
expect(payload.parts[0].text.indexOf('First')).toBeLessThan(payload.parts[0].text.indexOf('Second'));
|
|
56
|
-
expect(payload.parts[0].text).toContain('continuing the pre-compaction work');
|
|
57
|
-
expect(payload.parts[0].text).toContain('use it silently as background context');
|
|
58
|
-
expect(payload.parts[0].text).toContain('Only if no tasks or next steps remain');
|
|
59
|
-
expect(payload.parts[0].text).toContain('no more than one short paragraph');
|
|
60
|
-
const patch = requests.find((request) => request.method === 'PATCH');
|
|
61
|
-
expect(JSON.parse(patch.body).metadata.openchamber.context_obligatory_last_compaction_message_id).toBe('msg_summary');
|
|
62
|
-
expect(sessionReads).toBe(2);
|
|
63
|
-
runtime.stop();
|
|
43
|
+
expect(buildOpenCodeUrl).not.toHaveBeenCalled();
|
|
64
44
|
});
|
|
65
45
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const requests = [];
|
|
72
|
-
vi.stubGlobal('fetch', vi.fn(async (input, init = {}) => {
|
|
73
|
-
const url = new URL(typeof input === 'string' ? input : input.url);
|
|
74
|
-
requests.push({ path: url.pathname, method: init.method ?? 'GET', body: init.body });
|
|
75
|
-
if (url.pathname === '/session/ses_1' && init.method === 'PATCH') return json({});
|
|
76
|
-
if (url.pathname === '/session/ses_1') return json({
|
|
77
|
-
id: 'ses_1',
|
|
78
|
-
metadata: { openchamber: { knowledge_context_delivered: 'sig-before-compaction' } },
|
|
79
|
-
});
|
|
80
|
-
if (url.pathname === '/session/ses_1/message') return json([
|
|
81
|
-
{ info: { id: 'msg_agent', role: 'assistant', providerID: 'provider', modelID: 'model', agent: 'build' } },
|
|
82
|
-
{ info: { id: 'msg_summary', role: 'assistant', summary: true, time: { completed: 30 } } },
|
|
83
|
-
]);
|
|
84
|
-
if (url.pathname === '/session/ses_1/prompt_async') return json({});
|
|
85
|
-
throw new Error(`Unexpected ${url.pathname}`);
|
|
86
|
-
}));
|
|
87
|
-
const resolvePending = vi.fn(async () => ({
|
|
88
|
-
text: '## Pinned notes\n\n- Remember this.',
|
|
89
|
-
signature: 'sig-1',
|
|
90
|
-
}));
|
|
91
|
-
const runtime = createContextObligatoryRuntime({
|
|
92
|
-
buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
|
|
93
|
-
getOpenCodeAuthHeaders: () => ({}),
|
|
94
|
-
sessionKnowledgeRuntime: {
|
|
95
|
-
metadataKey: 'knowledge_context_delivered',
|
|
96
|
-
readPins: () => ({ notes: ['n1'], plans: [] }),
|
|
97
|
-
resolvePending,
|
|
98
|
-
},
|
|
46
|
+
it('starts working once both seams are wired', async () => {
|
|
47
|
+
const readSessionMetadata = vi.fn(async () => ({}));
|
|
48
|
+
const { runtime, buildOpenCodeUrl } = makeRuntime({
|
|
49
|
+
readSessionMetadata,
|
|
50
|
+
persistContextCursor: vi.fn(async () => undefined),
|
|
99
51
|
});
|
|
52
|
+
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ id: 'ses_1' }), {
|
|
53
|
+
status: 200, headers: { 'content-type': 'application/json' },
|
|
54
|
+
})));
|
|
100
55
|
|
|
101
|
-
await runtime.processPayload({
|
|
102
|
-
type: 'session.compacted',
|
|
103
|
-
properties: { sessionID: 'ses_1', directory: '/work/project' },
|
|
104
|
-
});
|
|
56
|
+
await runtime.processPayload({ type: 'session.compacted', properties: { sessionID: 'ses_1' } });
|
|
105
57
|
|
|
106
|
-
expect(
|
|
107
|
-
|
|
108
|
-
'',
|
|
109
|
-
{ notes: ['n1'], plans: [] },
|
|
110
|
-
);
|
|
111
|
-
const prompt = requests.find((request) => request.path.endsWith('/prompt_async'));
|
|
112
|
-
expect(JSON.parse(prompt.body).parts[0].text).toContain('Remember this.');
|
|
113
|
-
const patch = requests.find((request) => request.method === 'PATCH');
|
|
114
|
-
// Recorded with the cursor, so the next ordinary send does not repeat it.
|
|
115
|
-
expect(JSON.parse(patch.body).metadata.openchamber.knowledge_context_delivered).toBe('sig-1');
|
|
58
|
+
expect(buildOpenCodeUrl).toHaveBeenCalled();
|
|
59
|
+
expect(readSessionMetadata).toHaveBeenCalledWith('ses_1');
|
|
116
60
|
});
|
|
117
61
|
|
|
118
|
-
it('
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const url = new URL(typeof input === 'string' ? input : input.url);
|
|
122
|
-
requests.push({ path: url.pathname, method: init.method ?? 'GET', body: init.body });
|
|
123
|
-
if (url.pathname === '/session/ses_1' && init.method === 'PATCH') return json({});
|
|
124
|
-
if (url.pathname === '/session/ses_1') return json({
|
|
125
|
-
id: 'ses_1',
|
|
126
|
-
metadata: { openchamber: { context_obligatory_messages: [{ id: 'msg_1', createdAt: 10, role: 'user' }] } },
|
|
127
|
-
});
|
|
128
|
-
if (url.pathname === '/session/ses_1/message') return json([
|
|
129
|
-
{ info: { id: 'msg_agent', role: 'assistant', providerID: 'provider', modelID: 'model', agent: 'build' } },
|
|
130
|
-
{ info: { id: 'msg_summary', role: 'assistant', summary: true, time: { completed: 30 } } },
|
|
131
|
-
]);
|
|
132
|
-
if (url.pathname === '/session/ses_1/message/msg_1') return json({ parts: [{ type: 'text', text: 'Pinned message' }] });
|
|
133
|
-
if (url.pathname === '/session/ses_1/prompt_async') return json({});
|
|
134
|
-
throw new Error(`Unexpected ${url.pathname}`);
|
|
135
|
-
}));
|
|
136
|
-
const runtime = createContextObligatoryRuntime({
|
|
137
|
-
buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
|
|
138
|
-
getOpenCodeAuthHeaders: () => ({}),
|
|
139
|
-
sessionKnowledgeRuntime: {
|
|
140
|
-
metadataKey: 'knowledge_context_delivered',
|
|
141
|
-
readPins: () => ({ notes: ['n1'], plans: [] }),
|
|
142
|
-
resolvePending: async () => ({ text: 'Pinned notes block', signature: 'sig-1' }),
|
|
143
|
-
},
|
|
144
|
-
});
|
|
62
|
+
it('reaches no service while no cursor store is injected', async () => {
|
|
63
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
64
|
+
const { runtime, buildOpenCodeUrl } = makeRuntime();
|
|
145
65
|
|
|
146
66
|
await runtime.processPayload({ type: 'session.compacted', properties: { sessionID: 'ses_1' } });
|
|
147
67
|
|
|
148
|
-
|
|
149
|
-
// being interrupted twice.
|
|
150
|
-
const prompts = requests.filter((request) => request.path.endsWith('/prompt_async'));
|
|
151
|
-
expect(prompts).toHaveLength(1);
|
|
152
|
-
const text = JSON.parse(prompts[0].body).parts[0].text;
|
|
153
|
-
expect(text).toContain('Pinned notes block');
|
|
154
|
-
expect(text).toContain('Pinned message');
|
|
68
|
+
expect(buildOpenCodeUrl).not.toHaveBeenCalled();
|
|
155
69
|
});
|
|
156
70
|
|
|
157
|
-
it('
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
const url = new URL(typeof input === 'string' ? input : input.url);
|
|
161
|
-
requests.push({ path: url.pathname, method: init.method ?? 'GET' });
|
|
162
|
-
if (url.pathname === '/session/ses_1') return json({ id: 'ses_1', metadata: {} });
|
|
163
|
-
throw new Error(`Unexpected ${url.pathname}`);
|
|
164
|
-
}));
|
|
165
|
-
const runtime = createContextObligatoryRuntime({
|
|
166
|
-
buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
|
|
167
|
-
getOpenCodeAuthHeaders: () => ({}),
|
|
168
|
-
sessionKnowledgeRuntime: {
|
|
169
|
-
metadataKey: 'knowledge_context_delivered',
|
|
170
|
-
readPins: () => ({ notes: [], plans: [] }),
|
|
171
|
-
resolvePending: async () => ({ text: '', signature: 'sig-1' }),
|
|
172
|
-
},
|
|
173
|
-
});
|
|
71
|
+
it('explains itself once, not on every event', () => {
|
|
72
|
+
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
73
|
+
const { runtime } = makeRuntime();
|
|
174
74
|
|
|
175
|
-
|
|
75
|
+
runtime.processPayload({ type: 'session.compacted', properties: { sessionID: 'ses_1' } });
|
|
76
|
+
runtime.processPayload({ type: 'session.idle', properties: { sessionID: 'ses_2' } });
|
|
176
77
|
|
|
177
|
-
|
|
78
|
+
const notices = log.mock.calls.filter(([line]) => String(line).includes('[context-obligatory] parked'));
|
|
79
|
+
expect(notices).toHaveLength(1);
|
|
178
80
|
});
|
|
179
81
|
|
|
180
|
-
it('ignores
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
|
|
185
|
-
getOpenCodeAuthHeaders: () => ({}),
|
|
186
|
-
});
|
|
187
|
-
await runtime.processPayload({ type: 'session.status', properties: { sessionID: 'ses_1', status: { type: 'idle' } } });
|
|
188
|
-
expect(fetchImpl).not.toHaveBeenCalled();
|
|
82
|
+
it('ignores everything after stop', () => {
|
|
83
|
+
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
84
|
+
const { runtime } = makeRuntime();
|
|
85
|
+
|
|
189
86
|
runtime.stop();
|
|
87
|
+
runtime.processPayload({ type: 'session.compacted', properties: { sessionID: 'ses_1' } });
|
|
88
|
+
|
|
89
|
+
expect(log).not.toHaveBeenCalled();
|
|
190
90
|
});
|
|
191
91
|
});
|
|
@@ -10,6 +10,7 @@ This module contains the OpenChamber message-stream WebSocket protocol and runti
|
|
|
10
10
|
- `packages/web/server/lib/event-stream/global-ws-bridge.js`: browser-facing global WS bridge that subscribes clients to the shared global hub.
|
|
11
11
|
- `packages/web/server/lib/event-stream/directory-ws-bridge.js`: browser-facing per-directory WS bridge that owns one scoped upstream reader per connection.
|
|
12
12
|
- `packages/web/server/lib/event-stream/protocol.js`: path constants, SSE envelope parsing, and WebSocket frame serialization helpers.
|
|
13
|
+
- `packages/web/server/lib/event-stream/translate-v2.js`: the single place that maps OpenCode 2.x wire events onto the server's own event vocabulary.
|
|
13
14
|
- `packages/web/server/lib/event-stream/upstream-reader.js`: reusable upstream SSE reader with event-id tracking, stall recovery, and reconnect handling.
|
|
14
15
|
- `packages/web/server/lib/event-stream/runtime.js`: thin WebSocket server runtime for upgrade handling and path dispatch to the global/directory bridges.
|
|
15
16
|
- `packages/web/server/lib/event-stream/protocol.test.js`: unit tests for protocol helpers.
|
|
@@ -32,7 +33,13 @@ This module contains the OpenChamber message-stream WebSocket protocol and runti
|
|
|
32
33
|
- `createGlobalUiEventBroadcaster({ sseClients, wsClients, writeSseEvent })`: returns a broadcaster that fans out the same synthetic UI event to SSE and WS clients.
|
|
33
34
|
- `createMessageStreamWsRuntime(...)`: mounts the message-stream WS server, upgrade handler, and SSE-to-WS bridge onto the web HTTP server.
|
|
34
35
|
|
|
36
|
+
### Event translation
|
|
37
|
+
- `translateWireEvent(payload)`: one v2 wire event in, zero or more server-vocabulary events out.
|
|
38
|
+
- `wireEventDirectory(payload)`: the directory an event belongs to, read from `payload.location.directory`.
|
|
39
|
+
- `forwardTranslatedWireEvent(payload, handle)`: translate and forward, for consumers that take one handler.
|
|
40
|
+
|
|
35
41
|
### Coalescing helpers
|
|
42
|
+
- `createDeltaCoalescer(...)`: merges consecutive streaming fragments before replay and fan-out. In OpenCode 2.x the merged shapes are `session.text.delta`, `session.reasoning.delta` and `session.tool.input.delta` (v1 had a single `message.part.delta`); every other event is a barrier. Text and reasoning fragments are keyed by `data.ordinal`, tool input by `data.id`.
|
|
36
43
|
- `resolveDeltaCoalesceWindowMs(env)`: reads `OPENCHAMBER_EVENT_DELTA_COALESCE_MS`. Unset means 50ms, `0` turns coalescing off, and anything that is not a whole number from 0 to 1000 keeps the default with a warning.
|
|
37
44
|
|
|
38
45
|
### Upstream reader helpers
|
|
@@ -42,7 +49,10 @@ This module contains the OpenChamber message-stream WebSocket protocol and runti
|
|
|
42
49
|
|
|
43
50
|
## Runtime behavior
|
|
44
51
|
- Browser clients connect to the WS endpoints above.
|
|
45
|
-
- OpenChamber still fetches OpenCode upstream event streams over SSE
|
|
52
|
+
- OpenChamber still fetches OpenCode upstream event streams over SSE, now from `/api/event`.
|
|
53
|
+
- OpenCode 2.x sends no `id:` SSE lines: the event id is `payload.id` and the directory is `payload.location.directory`. Replay and per-directory routing read both out of the payload.
|
|
54
|
+
- Browser clients receive the RAW wire payload and translate it themselves. Server-side subscribers read `event.translated()` instead, so the translation happens once per event and only when something listens.
|
|
55
|
+
- v2 emits no `session.status` and no `session.idle` of its own: live status is synthesized from `session.execution.started|succeeded|interrupted|failed`. A user abort arrives as `session.idle` with `aborted: true`, not as an assistant message carrying `MessageAbortedError`. A `session.execution.interrupted` with `reason: "shutdown"` translates to nothing: OpenCode keeps the execution claim across a shutdown and resumes the turn after restart, so the session stays busy for server consumers (goal, queue, notifications) until the real terminal event arrives.
|
|
46
56
|
- The web server creates one shared global message-stream hub. OpenCode watcher side effects and global WS clients subscribe to that hub, so there is one upstream `/global/event` SSE reader for both server-side processing and browser fan-out.
|
|
47
57
|
- The global hub keeps a bounded replay buffer keyed by SSE `eventId` so reconnecting browser clients can receive buffered events after their requested `Last-Event-ID`.
|
|
48
58
|
- Replay retains at most 2,048 events and 8 MiB of UTF-8 wire frames. It stores encoded frames rather than retaining parsed payloads as well. Encoding is shared with live WS fanout. An oversized event still reaches live clients in full, but clears the retained suffix so replay cannot cross its gap. A missing replay cursor returns `null`, distinct from a complete empty tail; the bridge sends `ready` with `replayReset: true` and no partial replay. The client retires its cursor and requests authoritative repair, including during the early-boot reconnect grace period. A stopped hub retains its bounded suffix for clients reconnecting after the last socket closed.
|
|
@@ -83,6 +93,7 @@ The directory WS bridge and the SSE proxy (`/api/global/event`, used by Capacito
|
|
|
83
93
|
## Testing
|
|
84
94
|
- Run `bunx vitest run server/lib/event-stream` from `packages/web` for the whole module, including `delta-coalescer.test.js` and the resume-from-any-cursor cases in `global-hub.test.js`.
|
|
85
95
|
- Run `bun test packages/web/server/lib/event-stream/protocol.test.js`
|
|
96
|
+
- Run `bun test packages/web/server/lib/event-stream/translate-v2.test.js`
|
|
86
97
|
- Run `bun test packages/web/server/lib/event-stream/upstream-reader.test.js`
|
|
87
98
|
- Run `bun test packages/web/server/lib/event-stream/runtime.test.js`
|
|
88
99
|
- Run repo validation before finalizing: `bun run type-check`, `bun run lint`, `bun run build`
|
|
Binary file
|
|
@@ -4,17 +4,27 @@ import { createDeltaCoalescer, DELTA_COALESCE_WINDOW_MS, resolveDeltaCoalesceWin
|
|
|
4
4
|
|
|
5
5
|
let nextId = 0;
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// v2 wire shape: `{ type, id, data, location }`. Text and reasoning fragments
|
|
8
|
+
// are addressed by `ordinal`, tool-input fragments by the tool call `id`.
|
|
9
|
+
const delta = (ordinal, text, {
|
|
10
|
+
messageID = 'msg_1',
|
|
11
|
+
type = 'session.text.delta',
|
|
12
|
+
directory = '/repo',
|
|
13
|
+
sessionID = 'ses_1',
|
|
14
|
+
} = {}) => {
|
|
8
15
|
nextId += 1;
|
|
9
16
|
const id = String(nextId).padStart(6, '0');
|
|
10
|
-
const
|
|
17
|
+
const data = { sessionID, assistantMessageID: messageID, delta: text };
|
|
18
|
+
if (type === 'session.tool.input.delta') data.id = String(ordinal);
|
|
19
|
+
else data.ordinal = ordinal;
|
|
20
|
+
const payload = { id, type, location: { directory }, data };
|
|
11
21
|
return { envelope: { eventId: id, directory, payload }, payload };
|
|
12
22
|
};
|
|
13
23
|
|
|
14
|
-
const other = (type,
|
|
24
|
+
const other = (type, data = {}) => {
|
|
15
25
|
nextId += 1;
|
|
16
26
|
const id = String(nextId).padStart(6, '0');
|
|
17
|
-
const payload = { id, type,
|
|
27
|
+
const payload = { id, type, location: { directory: '/repo' }, data };
|
|
18
28
|
return { envelope: { eventId: id, directory: '/repo', payload }, payload };
|
|
19
29
|
};
|
|
20
30
|
|
|
@@ -52,14 +62,15 @@ const createHarness = (options = {}) => {
|
|
|
52
62
|
return { coalescer, emitted, advance, pendingTimers: () => timers.length };
|
|
53
63
|
};
|
|
54
64
|
|
|
55
|
-
const
|
|
56
|
-
const
|
|
65
|
+
const DELTA_TYPES = new Set(['session.text.delta', 'session.reasoning.delta', 'session.tool.input.delta']);
|
|
66
|
+
const isDelta = (event) => DELTA_TYPES.has(event.payload.type);
|
|
67
|
+
const keyOf = (event) => `${event.payload.data.assistantMessageID}:${event.payload.type}:${event.payload.data.ordinal ?? event.payload.data.id}`;
|
|
57
68
|
|
|
58
69
|
const textByKey = (events) => {
|
|
59
70
|
const text = new Map();
|
|
60
71
|
for (const event of events) {
|
|
61
72
|
if (!isDelta(event)) continue;
|
|
62
|
-
text.set(keyOf(event), (text.get(keyOf(event)) ?? '') + event.payload.
|
|
73
|
+
text.set(keyOf(event), (text.get(keyOf(event)) ?? '') + event.payload.data.delta);
|
|
63
74
|
}
|
|
64
75
|
return text;
|
|
65
76
|
};
|
|
@@ -67,7 +78,7 @@ const textByKey = (events) => {
|
|
|
67
78
|
describe('delta coalescer', () => {
|
|
68
79
|
it('emits the first delta after a quiet spell at once and unchanged', () => {
|
|
69
80
|
const { coalescer, emitted } = createHarness();
|
|
70
|
-
const first = delta(
|
|
81
|
+
const first = delta(1, 'He');
|
|
71
82
|
|
|
72
83
|
coalescer.push(first);
|
|
73
84
|
|
|
@@ -75,28 +86,28 @@ describe('delta coalescer', () => {
|
|
|
75
86
|
expect(emitted[0]).toBe(first);
|
|
76
87
|
});
|
|
77
88
|
|
|
78
|
-
it('merges following deltas for one
|
|
89
|
+
it('merges following deltas for one stream and carries the last fragment id', () => {
|
|
79
90
|
const { coalescer, emitted, advance, pendingTimers } = createHarness();
|
|
80
|
-
const fragments = [delta(
|
|
91
|
+
const fragments = [delta(1, 'He'), delta(1, 'll'), delta(1, 'o,'), delta(1, ' wor'), delta(1, 'ld')];
|
|
81
92
|
|
|
82
93
|
for (const fragment of fragments) coalescer.push(fragment);
|
|
83
94
|
expect(emitted).toHaveLength(1);
|
|
84
95
|
advance(50);
|
|
85
96
|
|
|
86
97
|
expect(emitted).toHaveLength(2);
|
|
87
|
-
expect(emitted[1].payload.
|
|
98
|
+
expect(emitted[1].payload.data).toEqual({ sessionID: 'ses_1', assistantMessageID: 'msg_1', ordinal: 1, delta: 'llo, world' });
|
|
88
99
|
expect(emitted[1].envelope.eventId).toBe(fragments[4].envelope.eventId);
|
|
89
100
|
expect(emitted[1].payload.id).toBe(fragments[4].payload.id);
|
|
90
101
|
expect(emitted[1].envelope.payload).toBe(emitted[1].payload);
|
|
91
102
|
expect(pendingTimers()).toBe(0);
|
|
92
103
|
// Upstream objects are never mutated: other hub subscribers may hold them.
|
|
93
|
-
expect(fragments.map((fragment) => fragment.payload.
|
|
104
|
+
expect(fragments.map((fragment) => fragment.payload.data.delta)).toEqual(['He', 'll', 'o,', ' wor', 'ld']);
|
|
94
105
|
});
|
|
95
106
|
|
|
96
107
|
it('never holds text longer than the window', () => {
|
|
97
108
|
const { coalescer, emitted, advance } = createHarness();
|
|
98
|
-
coalescer.push(delta(
|
|
99
|
-
coalescer.push(delta(
|
|
109
|
+
coalescer.push(delta(1, 'a'));
|
|
110
|
+
coalescer.push(delta(1, 'b'));
|
|
100
111
|
|
|
101
112
|
advance(49);
|
|
102
113
|
expect(emitted).toHaveLength(1);
|
|
@@ -104,104 +115,104 @@ describe('delta coalescer', () => {
|
|
|
104
115
|
expect(emitted).toHaveLength(2);
|
|
105
116
|
});
|
|
106
117
|
|
|
107
|
-
it('treats every other event as a barrier, so a delta never crosses a
|
|
118
|
+
it('treats every other event as a barrier, so a delta never crosses a stream snapshot', () => {
|
|
108
119
|
const { coalescer, emitted, advance } = createHarness();
|
|
109
|
-
const snapshot = other('
|
|
120
|
+
const snapshot = other('session.text.ended', { sessionID: 'ses_1', assistantMessageID: 'msg_1', ordinal: 1, text: 'abc' });
|
|
110
121
|
|
|
111
|
-
coalescer.push(delta(
|
|
112
|
-
coalescer.push(delta(
|
|
113
|
-
coalescer.push(delta(
|
|
122
|
+
coalescer.push(delta(1, 'a'));
|
|
123
|
+
coalescer.push(delta(1, 'b'));
|
|
124
|
+
coalescer.push(delta(1, 'c'));
|
|
114
125
|
coalescer.push(snapshot);
|
|
115
|
-
coalescer.push(delta(
|
|
116
|
-
coalescer.push(delta(
|
|
126
|
+
coalescer.push(delta(1, 'd'));
|
|
127
|
+
coalescer.push(delta(1, 'e'));
|
|
117
128
|
advance(50);
|
|
118
129
|
|
|
119
|
-
expect(emitted.map((event) => (isDelta(event) ? event.payload.
|
|
120
|
-
.toEqual(['a', 'bc', '
|
|
130
|
+
expect(emitted.map((event) => (isDelta(event) ? event.payload.data.delta : event.payload.type)))
|
|
131
|
+
.toEqual(['a', 'bc', 'session.text.ended', 'de']);
|
|
121
132
|
expect(emitted[2]).toBe(snapshot);
|
|
122
133
|
});
|
|
123
134
|
|
|
124
|
-
it('keeps several
|
|
135
|
+
it('keeps several streams pending at once and emits them in upstream id order', () => {
|
|
125
136
|
const { coalescer, emitted, advance } = createHarness();
|
|
126
|
-
coalescer.push(delta(
|
|
137
|
+
coalescer.push(delta(0, '.'));
|
|
127
138
|
|
|
128
|
-
coalescer.push(delta(
|
|
129
|
-
coalescer.push(delta(
|
|
130
|
-
coalescer.push(delta(
|
|
131
|
-
const lastB = delta(
|
|
139
|
+
coalescer.push(delta(1, 'a1'));
|
|
140
|
+
coalescer.push(delta(2, 'b1', { messageID: 'msg_2', sessionID: 'ses_2' }));
|
|
141
|
+
coalescer.push(delta(1, 'a2'));
|
|
142
|
+
const lastB = delta(2, 'b2', { messageID: 'msg_2', sessionID: 'ses_2' });
|
|
132
143
|
coalescer.push(lastB);
|
|
133
144
|
advance(50);
|
|
134
145
|
|
|
135
146
|
const merged = emitted.slice(1);
|
|
136
|
-
expect(merged.map((event) => event.payload.
|
|
147
|
+
expect(merged.map((event) => event.payload.data.delta)).toEqual(['a1a2', 'b1b2']);
|
|
137
148
|
expect(merged[1].envelope.eventId).toBe(lastB.envelope.eventId);
|
|
138
149
|
const ids = emitted.map((event) => event.envelope.eventId);
|
|
139
150
|
expect(ids).toEqual([...ids].sort());
|
|
140
151
|
});
|
|
141
152
|
|
|
142
|
-
it('does not merge the same
|
|
153
|
+
it('does not merge the same ordinal across directories, sessions, messages, or stream kinds', () => {
|
|
143
154
|
const { coalescer, emitted, advance } = createHarness();
|
|
144
|
-
coalescer.push(delta(
|
|
155
|
+
coalescer.push(delta(0, '.'));
|
|
145
156
|
|
|
146
|
-
coalescer.push(delta(
|
|
147
|
-
coalescer.push(delta(
|
|
148
|
-
coalescer.push(delta(
|
|
149
|
-
coalescer.push(delta(
|
|
150
|
-
coalescer.push(delta(
|
|
157
|
+
coalescer.push(delta(1, '1'));
|
|
158
|
+
coalescer.push(delta(1, '2', { directory: '/other' }));
|
|
159
|
+
coalescer.push(delta(1, '3', { messageID: 'msg_2' }));
|
|
160
|
+
coalescer.push(delta(1, '4', { type: 'session.reasoning.delta' }));
|
|
161
|
+
coalescer.push(delta(1, '5', { sessionID: 'ses_2' }));
|
|
151
162
|
advance(50);
|
|
152
163
|
|
|
153
|
-
expect(emitted.slice(1).map((event) => event.payload.
|
|
164
|
+
expect(emitted.slice(1).map((event) => event.payload.data.delta)).toEqual(['1', '2', '3', '4', '5']);
|
|
154
165
|
});
|
|
155
166
|
|
|
156
|
-
it('bounds pending text by characters and by
|
|
167
|
+
it('bounds pending text by characters and by streams, emitting early instead of dropping', () => {
|
|
157
168
|
const { coalescer, emitted } = createHarness({ maxPendingChars: 10, maxPendingKeys: 3 });
|
|
158
|
-
coalescer.push(delta(
|
|
169
|
+
coalescer.push(delta(0, '.'));
|
|
159
170
|
|
|
160
171
|
let peakChars = 0;
|
|
161
172
|
for (let index = 0; index < 40; index += 1) {
|
|
162
|
-
coalescer.push(delta(
|
|
173
|
+
coalescer.push(delta(1, 'xyz'));
|
|
163
174
|
peakChars = Math.max(peakChars, coalescer.pendingSize().chars);
|
|
164
175
|
}
|
|
165
176
|
expect(peakChars).toBeLessThan(10 + 3);
|
|
166
|
-
expect(textByKey(emitted).get('msg_1:
|
|
177
|
+
expect(textByKey(emitted).get('msg_1:session.text.delta:1')).toBe('xyz'.repeat(40));
|
|
167
178
|
|
|
168
179
|
let peakKeys = 0;
|
|
169
180
|
for (let index = 0; index < 20; index += 1) {
|
|
170
|
-
coalescer.push(delta(
|
|
181
|
+
coalescer.push(delta(100 + index, 'k'));
|
|
171
182
|
peakKeys = Math.max(peakKeys, coalescer.pendingSize().keys);
|
|
172
183
|
}
|
|
173
184
|
expect(peakKeys).toBeLessThanOrEqual(3);
|
|
174
185
|
coalescer.flush();
|
|
175
|
-
expect(emitted.filter((event) => event.payload.
|
|
186
|
+
expect(emitted.filter((event) => event.payload.data.delta === 'k')).toHaveLength(20);
|
|
176
187
|
});
|
|
177
188
|
|
|
178
189
|
it('flushes on demand and leaves nothing pending', () => {
|
|
179
190
|
const { coalescer, emitted, pendingTimers } = createHarness();
|
|
180
|
-
coalescer.push(delta(
|
|
181
|
-
coalescer.push(delta(
|
|
191
|
+
coalescer.push(delta(1, 'a'));
|
|
192
|
+
coalescer.push(delta(1, 'b'));
|
|
182
193
|
|
|
183
194
|
coalescer.flush();
|
|
184
195
|
|
|
185
|
-
expect(emitted.map((event) => event.payload.
|
|
196
|
+
expect(emitted.map((event) => event.payload.data.delta)).toEqual(['a', 'b']);
|
|
186
197
|
expect(coalescer.pendingSize()).toEqual({ keys: 0, chars: 0 });
|
|
187
198
|
expect(pendingTimers()).toBe(0);
|
|
188
199
|
});
|
|
189
200
|
|
|
190
201
|
it('passes a malformed delta through as a barrier instead of guessing', () => {
|
|
191
202
|
const { coalescer, emitted } = createHarness();
|
|
192
|
-
const malformed = other('
|
|
203
|
+
const malformed = other('session.text.delta', { sessionID: 'ses_1', assistantMessageID: 'msg_1', ordinal: 1, delta: 42 });
|
|
193
204
|
|
|
194
|
-
coalescer.push(delta(
|
|
195
|
-
coalescer.push(delta(
|
|
205
|
+
coalescer.push(delta(1, 'a'));
|
|
206
|
+
coalescer.push(delta(1, 'b'));
|
|
196
207
|
coalescer.push(malformed);
|
|
197
208
|
|
|
198
|
-
expect(emitted.map((event) => event.payload.
|
|
209
|
+
expect(emitted.map((event) => event.payload.data.delta)).toEqual(['a', 'b', 42]);
|
|
199
210
|
expect(emitted[2]).toBe(malformed);
|
|
200
211
|
});
|
|
201
212
|
|
|
202
213
|
it('is a pass-through when the window is zero', () => {
|
|
203
214
|
const { coalescer, emitted } = createHarness({ windowMs: 0 });
|
|
204
|
-
const events = [delta(
|
|
215
|
+
const events = [delta(1, 'a'), delta(1, 'b'), delta(1, 'c')];
|
|
205
216
|
|
|
206
217
|
for (const event of events) coalescer.push(event);
|
|
207
218
|
|
|
@@ -226,11 +237,11 @@ describe('delta coalescer', () => {
|
|
|
226
237
|
let event;
|
|
227
238
|
if (roll < 0.9) {
|
|
228
239
|
const part = Math.floor(random() * partCount);
|
|
229
|
-
event = delta(
|
|
240
|
+
event = delta(part, `${index}|`.repeat(1 + Math.floor(random() * 3)), { messageID: `msg_${part % 2}` });
|
|
230
241
|
} else if (roll < 0.95) {
|
|
231
|
-
event = other('
|
|
242
|
+
event = other('session.text.ended', { sessionID: 'ses_1', assistantMessageID: 'msg_0', ordinal: Math.floor(random() * partCount), text: '' });
|
|
232
243
|
} else {
|
|
233
|
-
event = other('session.
|
|
244
|
+
event = other('session.execution.started', { sessionID: 'ses_1' });
|
|
234
245
|
}
|
|
235
246
|
raw.push(event);
|
|
236
247
|
coalescer.push(event);
|
|
@@ -264,12 +275,12 @@ describe('delta coalescer', () => {
|
|
|
264
275
|
|
|
265
276
|
// 150 fragments a second for two seconds, the rate measured from OpenCode.
|
|
266
277
|
for (let index = 0; index < 300; index += 1) {
|
|
267
|
-
coalescer.push(delta(
|
|
278
|
+
coalescer.push(delta(1, 'ab'));
|
|
268
279
|
advance(index % 3 === 2 ? 8 : 6);
|
|
269
280
|
}
|
|
270
281
|
advance(50);
|
|
271
282
|
|
|
272
|
-
expect(textByKey(emitted).get('msg_1:
|
|
283
|
+
expect(textByKey(emitted).get('msg_1:session.text.delta:1')).toBe('ab'.repeat(300));
|
|
273
284
|
expect(emitted.length).toBeLessThanOrEqual(45);
|
|
274
285
|
});
|
|
275
286
|
});
|
|
@@ -106,7 +106,7 @@ export function acceptDirectoryMessageStreamWsConnection({
|
|
|
106
106
|
buildUrlFailed = false;
|
|
107
107
|
let targetUrl;
|
|
108
108
|
try {
|
|
109
|
-
targetUrl = new URL(buildOpenCodeUrl('/event', ''));
|
|
109
|
+
targetUrl = new URL(buildOpenCodeUrl('/api/event', ''));
|
|
110
110
|
} catch {
|
|
111
111
|
buildUrlFailed = true;
|
|
112
112
|
throw new Error('OpenCode service unavailable');
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
|
|
3
3
|
import { createUpstreamSseReader } from './upstream-reader.js';
|
|
4
4
|
import { serializeMessageStreamWsEvent } from './protocol.js';
|
|
5
|
+
import { translateWireEvent } from './translate-v2.js';
|
|
5
6
|
import { createDeltaCoalescer, DELTA_COALESCE_WINDOW_MS } from './delta-coalescer.js';
|
|
6
7
|
|
|
7
8
|
// Raised from 512 → 2048 to improve recovery after brief disconnects during
|
|
@@ -69,6 +70,7 @@ export function createGlobalMessageStreamHub({
|
|
|
69
70
|
? envelope.eventId
|
|
70
71
|
: `${replayIdPrefix}${String(++replaySequence).padStart(12, '0')}`;
|
|
71
72
|
let serializedFrame;
|
|
73
|
+
let translated;
|
|
72
74
|
return {
|
|
73
75
|
envelope,
|
|
74
76
|
payload,
|
|
@@ -78,6 +80,13 @@ export function createGlobalMessageStreamHub({
|
|
|
78
80
|
serializedFrame ??= serializeMessageStreamWsEvent(payload, { directory, eventId });
|
|
79
81
|
return serializedFrame;
|
|
80
82
|
},
|
|
83
|
+
// Browser clients receive the raw wire payload and translate it
|
|
84
|
+
// themselves; server-side subscribers read this instead. Translating
|
|
85
|
+
// lazily keeps the cost off the WS fan-out path when nothing listens.
|
|
86
|
+
translated() {
|
|
87
|
+
translated ??= translateWireEvent(payload);
|
|
88
|
+
return translated;
|
|
89
|
+
},
|
|
81
90
|
};
|
|
82
91
|
};
|
|
83
92
|
|
|
@@ -123,7 +132,7 @@ export function createGlobalMessageStreamHub({
|
|
|
123
132
|
buildUrl: () => {
|
|
124
133
|
buildUrlFailed = false;
|
|
125
134
|
try {
|
|
126
|
-
return new URL(buildOpenCodeUrl('/
|
|
135
|
+
return new URL(buildOpenCodeUrl('/api/event', ''));
|
|
127
136
|
} catch {
|
|
128
137
|
buildUrlFailed = true;
|
|
129
138
|
throw new Error('OpenCode service unavailable');
|