@aiworker/web 1.22.0 → 1.23.0
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 +4 -4
- package/bin/cli.js +14 -4
- package/bin/cli.test.js +165 -73
- package/bin/lib/cli-api-target.js +2 -2
- package/bin/lib/cli-args.js +119 -119
- package/bin/lib/cli-lifecycle.js +9 -9
- package/bin/lib/cli-network.js +3 -3
- package/bin/lib/cli-paths.js +3 -3
- package/bin/lib/cli-ports.js +2 -2
- package/bin/lib/cli-process.js +4 -4
- package/bin/lib/cli-startup.js +64 -15
- package/bin/lib/cli-tunnel-profiles.js +1 -1
- package/bin/lib/cli-tunnel-utils.js +2 -2
- package/bin/lib/commands-connect-url.js +19 -19
- package/bin/lib/commands-lifecycle.js +27 -27
- package/bin/lib/commands-logs.js +5 -5
- package/bin/lib/commands-models.js +1 -1
- package/bin/lib/commands-projects.js +1 -1
- package/bin/lib/commands-schedule.js +1 -1
- package/bin/lib/commands-serve.js +29 -29
- package/bin/lib/commands-session.js +1 -1
- package/bin/lib/commands-startup.js +45 -10
- package/bin/lib/commands-status.js +1 -1
- package/bin/lib/commands-tunnel.js +40 -40
- package/bin/lib/commands-update.js +1 -1
- package/bin/lib/commands-update.test.js +6 -6
- package/dist/assets/{CodeMirrorCommentWidgets-B3u7UIcE.js → CodeMirrorCommentWidgets-B1Gizi3a.js} +1 -1
- package/dist/assets/DiffView-DXjVsEOw.js +4 -0
- package/dist/assets/DirtyBranchSwitchDialog-aig6zJjg.js +1 -0
- package/dist/assets/FilesView-CJdG0-wk.js +17 -0
- package/dist/assets/GitView-BXsS3jG6.js +6 -0
- package/dist/assets/{JsonTreeView-B0BdZEHG.js → JsonTreeView-D_hpXBnF.js} +1 -1
- package/dist/assets/LinearIssuesView-BoUiLQG1.js +1 -0
- package/dist/assets/MarkdownImageGallery-B_B_a9KS.js +1 -0
- package/dist/assets/MarkdownRendererImpl-xgyyLiBM.js +12 -0
- package/dist/assets/OnboardingScreen-DIp84OWt.js +1 -0
- package/dist/assets/{PatchDiff-DvfqDql7.js → PatchDiff-8p6nLJaU.js} +1 -1
- package/dist/assets/PierreDiffViewer-DjC8nTk9.js +161 -0
- package/dist/assets/PlanView-CJTmOF6I.js +5 -0
- package/dist/assets/SettingsView-DD-JqIJx.js +28 -0
- package/dist/assets/{SettingsWindow-i45U1Cy1.js → SettingsWindow-Cpb4hM6q.js} +1 -1
- package/dist/assets/SymbolsNerdFontMono-Regular-aK5vsLov.woff2 +0 -0
- package/dist/assets/{ToolOutputDialog-BTwyh5OF.js → ToolOutputDialog-DfaKZXNU.js} +6 -6
- package/dist/assets/ToolPartDiffPreview-DtZLzrSK.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-DD8lDVej.js → VirtualizedFileDiff-C8i9HpTB.js} +1 -1
- package/dist/assets/WalkthroughView-DeKMvZlL.js +4 -0
- package/dist/assets/{computeVirtualFileMetrics-BMIRgMHv.js → computeVirtualFileMetrics-BBI7ZwrC.js} +2 -2
- package/dist/assets/contextPanelEmbeddedChat-BdA6bdky.js +1 -0
- package/dist/assets/de-DCrsQ5zS.js +10 -0
- package/dist/assets/es-Dwv4YfH6.js +16 -0
- package/dist/assets/fr-BCWg4Cnx.js +14 -0
- package/dist/assets/ghostty-vt-DdA0Zryv.wasm +0 -0
- package/dist/assets/index-B68h5S5B.css +1 -0
- package/dist/assets/index-DVwMarHI.js +39 -0
- package/dist/assets/{index-CODwDsU_.js → index-jXSK5RrB.js} +1 -1
- package/dist/assets/{isDiffPlainText-BqayB8d3.js → isDiffPlainText-B_x_rVon.js} +1 -1
- package/dist/assets/{isFilePlainText-QfoiKJNY.js → isFilePlainText-DW_xMer1.js} +1 -1
- package/dist/assets/ja-Bruw3RYC.js +16 -0
- package/dist/assets/ko-idhXGF8j.js +16 -0
- package/dist/assets/main-BkyaQhJX.js +930 -0
- package/dist/assets/{main-C5DVOxc6.js → main-CzdPuL0s.js} +2 -2
- package/dist/assets/markdownCore-Cn4Tyejo.js +5 -0
- package/dist/assets/{miniChat-DUJ4irZc.js → miniChat-CCkqKOLp.js} +2 -2
- package/dist/assets/{mobile-CcaxB0n4.js → mobile-mvxTo055.js} +2 -2
- package/dist/assets/patchFileDiff-Csk4iNUi.js +13 -0
- package/dist/assets/pl-CGZWUqZJ.js +16 -0
- package/dist/assets/pt-BR-B8V7u7tA.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-BxoyujA5.js +2 -0
- package/dist/assets/renderMobileApp-B48CsP4r.js +25 -0
- package/dist/assets/runtimeEndpointReset-Bg5gx18H.js +17 -0
- package/dist/assets/scheduledTasksApi-BW0Etzwz.js +1 -0
- package/dist/assets/{shikiHighlight-DYOWuY65.js → shikiHighlight-CRl0SUC9.js} +1 -1
- package/dist/assets/surface-oMvhsog3.js +1 -0
- package/dist/assets/tr-BK_KiO2V.js +7 -0
- package/dist/assets/uk-CG74xV8N.js +16 -0
- package/dist/assets/useAppFontEffects-CPKeOpUo.js +7284 -0
- package/dist/assets/useGitComparison-DH220WmO.js +1 -0
- package/dist/assets/useInlineCommentController-BiWz4AVb.js +2 -0
- package/dist/assets/useRootScrollLock-BS4-kr7P.js +1 -0
- package/dist/assets/{vendor-base-ui-BeMD8X_w.js → vendor-base-ui-DdV8vhHn.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-DFZik9R2.js → vendor-codemirror-language-data-Bg4AEeCr.js} +2 -2
- package/dist/assets/vendor-zod-BPAANqvH.js +39 -0
- package/dist/assets/{web-components-Bhg83tjf.js → web-components-z7hfnS8n.js} +1 -1
- package/dist/assets/zh-CN-T4AXofZI.js +16 -0
- package/dist/assets/zh-TW-Dte40sxp.js +16 -0
- package/dist/index.html +21 -59
- package/dist/mini-chat.html +7 -7
- package/dist/mobile.html +5 -5
- package/package.json +3 -4
- package/server/index.d.ts +15 -0
- package/server/index.js +158 -89
- package/server/lib/agent-memory/actions.js +2 -2
- package/server/lib/agent-memory/feature-flag.js +1 -1
- package/server/lib/agent-memory/feature-flag.test.js +8 -8
- package/server/lib/agent-memory/routes.js +2 -2
- package/server/lib/agent-tool/runtime.js +37 -54
- package/server/lib/agent-tool/runtime.test.js +59 -59
- package/server/lib/browser-control/broker.js +3 -3
- package/server/lib/browser-control/broker.test.js +1 -1
- package/server/lib/changelog/update-notes.js +82 -0
- package/server/lib/changelog/update-notes.test.js +59 -0
- package/server/lib/client-auth/remote-clients.test.js +5 -5
- package/server/lib/context-obligatory/runtime.js +7 -7
- package/server/lib/context-obligatory/runtime.test.js +5 -5
- package/server/lib/data-dir-migration.js +40 -0
- package/server/lib/data-dir-migration.test.js +59 -0
- package/server/lib/dev-servers/parse.js +2 -2
- package/server/lib/dev-servers/parse.test.js +2 -2
- package/server/lib/dev-servers/routes.js +1 -1
- package/server/lib/dev-tunnel/DOCUMENTATION.md +13 -4
- package/server/lib/dev-tunnel/client.js +1 -1
- package/server/lib/dev-tunnel/runtime.js +5 -5
- package/server/lib/dev-tunnel/tunnel.test.js +21 -1
- package/server/lib/dictation/local/model-catalog.js +1 -1
- package/server/lib/dictation/local/worker-process.js +2 -2
- package/server/lib/event-stream/directory-ws-bridge.js +1 -1
- package/server/lib/event-stream/global-ws-bridge.js +1 -1
- package/server/lib/event-stream/protocol.test.js +4 -4
- package/server/lib/event-stream/runtime.test.js +18 -18
- package/server/lib/fs/DOCUMENTATION.md +5 -0
- package/server/lib/fs/routes.js +68 -26
- package/server/lib/fs/routes.test.js +208 -22
- package/server/lib/git/DOCUMENTATION.md +32 -6
- package/server/lib/git/identity-storage.js +1 -1
- package/server/lib/git/issue-2746-longpaths.test.js +4 -4
- package/server/lib/git/routes.js +41 -5
- package/server/lib/git/service.js +300 -224
- package/server/lib/git/service.test.js +596 -28
- package/server/lib/github/auth.js +8 -8
- package/server/lib/github/pr-status.js +1 -1
- package/server/lib/github/routes.js +2 -2
- package/server/lib/inherited-env.js +17 -10
- package/server/lib/inherited-env.test.js +10 -11
- package/server/lib/linear/auth.js +8 -8
- package/server/lib/linear/auth.test.js +24 -24
- package/server/lib/linear/issues.test.js +6 -6
- package/server/lib/linear/mapping.test.js +4 -4
- package/server/lib/linear/oauth.js +2 -2
- package/server/lib/linear/oauth.test.js +14 -14
- package/server/lib/linear/routes.js +5 -5
- package/server/lib/linear/routes.test.js +16 -16
- package/server/lib/linear/status-runtime.test.js +9 -9
- package/server/lib/linear/status.js +1 -1
- package/server/lib/linear/status.test.js +15 -15
- package/server/lib/linear/teams.test.js +4 -4
- package/server/lib/magic-prompts/routes.js +2 -2
- package/server/lib/mcp-reconnect/DOCUMENTATION.md +55 -0
- package/server/lib/mcp-reconnect/runtime.js +122 -0
- package/server/lib/mcp-reconnect/runtime.test.js +140 -0
- package/server/lib/message-queue/DOCUMENTATION.md +161 -0
- package/server/lib/message-queue/runtime.js +870 -0
- package/server/lib/message-queue/runtime.test.js +482 -0
- package/server/lib/network-defaults.js +23 -0
- package/server/lib/network-defaults.test.js +52 -0
- package/server/lib/notifications/apns-runtime.js +17 -17
- package/server/lib/notifications/apns-runtime.test.js +11 -11
- package/server/lib/notifications/emitter-runtime.js +1 -1
- package/server/lib/notifications/emitter-runtime.test.js +1 -1
- package/server/lib/notifications/push-runtime.js +7 -7
- package/server/lib/notifications/routes.js +1 -1
- package/server/lib/notifications/runtime.js +1 -1
- package/server/lib/openchamber-control/action-resolution.test.js +10 -10
- package/server/lib/openchamber-control/actions.js +24 -24
- package/server/lib/openchamber-control/error.js +4 -4
- package/server/lib/openchamber-control/routes.js +2 -2
- package/server/lib/openchamber-control/routes.test.js +5 -5
- package/server/lib/openchamber-control/screenshots.js +1 -1
- package/server/lib/openchamber-control/screenshots.test.js +1 -1
- package/server/lib/openchamber-control/service.js +50 -50
- package/server/lib/openchamber-control/service.test.js +5 -5
- package/server/lib/openchamber-sessions/routes.js +124 -22
- package/server/lib/openchamber-sessions/routes.test.js +99 -11
- package/server/lib/opencode/DOCUMENTATION.md +30 -6
- package/server/lib/opencode/auth.js +1 -1
- package/server/lib/opencode/bootstrap-runtime.js +8 -6
- package/server/lib/opencode/cli-options.js +8 -8
- package/server/lib/opencode/config-paths.test.js +116 -0
- package/server/lib/opencode/core-routes.js +8 -7
- package/server/lib/opencode/core-routes.test.js +11 -11
- package/server/lib/opencode/env-config.js +5 -5
- package/server/lib/opencode/env-config.test.js +7 -7
- package/server/lib/opencode/env-runtime.js +10 -10
- package/server/lib/opencode/env-runtime.test.js +18 -18
- package/server/lib/opencode/feature-routes-runtime.js +19 -12
- package/server/lib/opencode/hmr-state-runtime.js +2 -2
- package/server/lib/opencode/hmr-state-runtime.test.js +1 -1
- package/server/lib/opencode/input-history-scope.js +14 -0
- package/server/lib/opencode/lifecycle.js +8 -8
- package/server/lib/opencode/lifecycle.test.js +4 -4
- package/server/lib/opencode/managed-plugin-config.js +35 -0
- package/server/lib/opencode/managed-process-registry.js +3 -3
- package/server/lib/opencode/mcp-oauth-callback.test.js +2 -2
- package/server/lib/opencode/openchamber-routes.js +79 -19
- package/server/lib/opencode/openchamber-routes.test.js +139 -12
- package/server/lib/opencode/plugins.js +2 -2
- package/server/lib/opencode/plugins.test.js +1 -1
- package/server/lib/opencode/project-icon-routes.js +2 -2
- package/server/lib/opencode/project-icon-routes.test.js +1 -1
- package/server/lib/opencode/providers.js +44 -2
- package/server/lib/opencode/providers.test.js +131 -4
- package/server/lib/opencode/proxy.js +2 -2
- package/server/lib/opencode/pwa-manifest-routes.js +1 -1
- package/server/lib/opencode/restart-session-recovery.test.js +1 -1
- package/server/lib/opencode/routes-upgrade.test.js +4 -4
- package/server/lib/opencode/routes.js +19 -17
- package/server/lib/opencode/server-startup-runtime.js +6 -6
- package/server/lib/opencode/session-runtime.js +3 -3
- package/server/lib/opencode/session-runtime.test.js +6 -6
- package/server/lib/opencode/settings-files.js +214 -0
- package/server/lib/opencode/settings-helpers.js +116 -44
- package/server/lib/opencode/settings-helpers.test.js +354 -12
- package/server/lib/opencode/settings-registry.json +747 -0
- package/server/lib/opencode/settings-runtime.js +141 -20
- package/server/lib/opencode/settings-runtime.test.js +191 -4
- package/server/lib/opencode/shared.js +5 -2
- package/server/lib/opencode/shared.test.js +7 -7
- package/server/lib/opencode/shutdown-runtime.js +2 -0
- package/server/lib/opencode/skill-routes.js +2 -1
- package/server/lib/opencode/snippets.js +1 -2
- package/server/lib/opencode/startup-performance.js +1 -1
- package/server/lib/opencode/startup-performance.test.js +6 -6
- package/server/lib/opencode/static-routes-runtime.js +8 -8
- package/server/lib/opencode/static-routes-runtime.test.js +7 -7
- package/server/lib/opencode/upgrade-capability.js +2 -2
- package/server/lib/opencode/upgrade-capability.test.js +3 -3
- package/server/lib/package-manager.js +17 -42
- package/server/lib/package-manager.test.js +19 -19
- package/server/lib/permission-auto-accept/runtime.js +1 -1
- package/server/lib/project-context/DOCUMENTATION.md +25 -1
- package/server/lib/project-context/routes.http.test.js +18 -0
- package/server/lib/project-context/routes.js +16 -1
- package/server/lib/project-context/runtime.js +248 -23
- package/server/lib/project-context/runtime.test.js +106 -3
- package/server/lib/projects/DOCUMENTATION.md +76 -0
- package/server/lib/projects/project-config.js +144 -11
- package/server/lib/projects/project-id.js +17 -0
- package/server/lib/projects/project-setup.js +409 -0
- package/server/lib/projects/project-setup.test.js +557 -0
- package/server/lib/projects/routes.js +59 -0
- package/server/lib/quota/DOCUMENTATION.md +40 -2
- package/server/lib/quota/credentials/providers.js +4 -0
- package/server/lib/quota/credentials/store.js +4 -4
- package/server/lib/quota/credentials/store.test.js +8 -8
- package/server/lib/quota/index.js +1 -0
- package/server/lib/quota/providers/cline-pass.js +125 -0
- package/server/lib/quota/providers/cline-pass.test.js +103 -0
- package/server/lib/quota/providers/exe-dev.js +75 -0
- package/server/lib/quota/providers/exe-dev.test.js +47 -0
- package/server/lib/quota/providers/hyper.js +118 -0
- package/server/lib/quota/providers/hyper.test.js +126 -0
- package/server/lib/quota/providers/index.js +22 -0
- package/server/lib/quota/providers/neuralwatt.js +3 -6
- package/server/lib/quota/providers/neuralwatt.test.js +7 -7
- package/server/lib/quota/providers/ollama-cloud.js +32 -1
- package/server/lib/quota/providers/ollama-cloud.test.js +33 -1
- package/server/lib/quota/providers/opencode-go.js +3 -2
- package/server/lib/quota/providers/opencode-go.test.js +35 -6
- package/server/lib/quota/providers/openrouter.js +119 -24
- package/server/lib/quota/providers/openrouter.test.js +330 -0
- package/server/lib/quota/providers/xai.js +1 -1
- package/server/lib/quota/routes.js +2 -0
- package/server/lib/quota/utils/auth.js +1 -1
- package/server/lib/relay/DOCUMENTATION.md +56 -1
- package/server/lib/relay/cross-compat.test.js +26 -0
- package/server/lib/relay/downstream-scheduler.js +154 -0
- package/server/lib/relay/downstream-scheduler.test.js +162 -0
- package/server/lib/relay/e2ee.js +10 -8
- package/server/lib/relay/flow-control.test.js +177 -0
- package/server/lib/relay/host-client.js +38 -9
- package/server/lib/relay/host-client.test.js +9 -1
- package/server/lib/relay/host-lock.js +1 -1
- package/server/lib/relay/service.js +5 -5
- package/server/lib/relay/tunnel-codec.js +9 -0
- package/server/lib/relay/tunnel-host.js +69 -18
- package/server/lib/relay/tunnel-host.test.js +9 -1
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +2 -2
- package/server/lib/scheduled-tasks/routes.js +5 -5
- package/server/lib/scheduled-tasks/service.js +25 -25
- package/server/lib/scheduled-tasks/service.test.js +1 -1
- package/server/lib/security/bind-host.js +4 -4
- package/server/lib/security/bind-host.test.js +1 -1
- package/server/lib/security/request-security.js +11 -5
- package/server/lib/security/request-security.test.js +14 -14
- package/server/lib/session-assist/runtime.js +15 -18
- package/server/lib/session-folders/routes.js +2 -2
- package/server/lib/session-folders/routes.test.js +8 -8
- package/server/lib/session-goal/DOCUMENTATION.md +20 -7
- package/server/lib/session-goal/create.js +4 -2
- package/server/lib/session-goal/create.test.js +1 -1
- package/server/lib/session-goal/objectives.js +4 -4
- package/server/lib/session-goal/routes.js +1 -1
- package/server/lib/session-goal/runtime.js +91 -30
- package/server/lib/session-goal/runtime.test.js +428 -2
- package/server/lib/session-knowledge/runtime.js +11 -11
- package/server/lib/session-knowledge/runtime.test.js +5 -5
- package/server/lib/skills-catalog/DOCUMENTATION.md +1 -1
- package/server/lib/skills-catalog/cache.test.js +2 -2
- package/server/lib/skills-catalog/disk-cache.js +5 -5
- package/server/lib/skills-catalog/github-meta.test.js +2 -2
- package/server/lib/skills-catalog/install.js +3 -2
- package/server/lib/small-model/DOCUMENTATION.md +7 -2
- package/server/lib/small-model/call.js +18 -7
- package/server/lib/small-model/call.test.js +58 -6
- package/server/lib/small-model/index.js +12 -16
- package/server/lib/small-model/index.test.js +1 -1
- package/server/lib/small-model/resolve.js +1 -1
- package/server/lib/small-model/resolve.test.js +1 -1
- package/server/lib/small-model/routes.js +2 -1
- package/server/lib/small-model/runtime-providers.js +15 -7
- package/server/lib/small-model/runtime-providers.test.js +7 -1
- package/server/lib/system-prompt/runtime.js +3 -22
- package/server/lib/system-prompt/runtime.test.js +3 -3
- package/server/lib/terminal/DOCUMENTATION.md +14 -10
- package/server/lib/terminal/runtime.js +190 -31
- package/server/lib/terminal/runtime.test.js +572 -38
- package/server/lib/terminal/shells.js +20 -3
- package/server/lib/terminal/shells.test.js +33 -2
- package/server/lib/tts/base-url.js +3 -3
- package/server/lib/tts/routes.test.js +20 -20
- package/server/lib/ui-auth/ui-auth.js +12 -9
- package/server/lib/ui-auth/ui-auth.test.js +19 -3
- package/server/lib/ui-auth/ui-passkeys.js +7 -7
- package/server/lib/walkthrough/DOCUMENTATION.md +33 -14
- package/server/lib/walkthrough/index.js +1 -0
- package/server/lib/walkthrough/jobs.test.js +1 -1
- package/server/lib/walkthrough/language.test.js +1 -1
- package/server/lib/walkthrough/model-settings.js +11 -15
- package/server/lib/walkthrough/model-settings.test.js +1 -1
- package/server/lib/walkthrough/prompt.js +3 -1
- package/server/lib/walkthrough/pull-request.test.js +5 -5
- package/server/lib/walkthrough/reproduce-2607.test.js +2 -2
- package/server/lib/walkthrough/routes.test.js +26 -5
- package/server/lib/walkthrough/sources.js +23 -2
- package/server/lib/walkthrough/store.js +3 -3
- package/server/lib/walkthrough/store.test.js +1 -1
- package/server/opencode-proxy.test.js +2 -2
- package/server/proxy-headers.js +1 -1
- package/server/sse-routes.test.js +4 -4
- package/README.md.bak +0 -250
- package/bin/lib/DOCUMENTATION.md.bak +0 -163
- package/dist/assets/CommitSection-SGBCAW0w.js +0 -1
- package/dist/assets/DiffView-DluiRSla.js +0 -4
- package/dist/assets/FilesView-Bdq8ppva.js +0 -17
- package/dist/assets/GitView-Znn7VJcP.js +0 -6
- package/dist/assets/LinearIssuesView-jP-hXyac.js +0 -1
- package/dist/assets/MarkdownImageGallery-ClOvuZxC.js +0 -1
- package/dist/assets/MarkdownRendererImpl-Dt9mjTRN.js +0 -12
- package/dist/assets/OnboardingScreen-D2DG7K6b.js +0 -1
- package/dist/assets/PierreDiffViewer-B7m3f0d7.js +0 -157
- package/dist/assets/PlanView-B0F3BtPZ.js +0 -5
- package/dist/assets/SettingsView-BBW-S-gh.js +0 -28
- package/dist/assets/ToolPartDiffPreview-CUNe28U1.js +0 -8
- package/dist/assets/WalkthroughView-egQgxel3.js +0 -4
- package/dist/assets/contextPanelEmbeddedChat-C3fjDbdg.js +0 -1
- package/dist/assets/de-C1P6X5Rs.js +0 -9
- package/dist/assets/es-CDgaF1_t.js +0 -15
- package/dist/assets/fr-Bgxqvvz_.js +0 -13
- package/dist/assets/index-C57jT0Sa.css +0 -1
- package/dist/assets/index-DW_E3rnL.js +0 -7
- package/dist/assets/ja-BDRrEcNP.js +0 -15
- package/dist/assets/ko-Ba8sQV79.js +0 -15
- package/dist/assets/main-Bv5Pkngh.js +0 -928
- package/dist/assets/markdownCore-CZwzz5gw.js +0 -3
- package/dist/assets/patchFileDiff-yNfWXN4E.js +0 -10
- package/dist/assets/pl-Dioz29BI.js +0 -15
- package/dist/assets/pt-BR-DIs6ElwV.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-BcKLHXKe.js +0 -2
- package/dist/assets/renderMobileApp-D9yQJy0L.js +0 -25
- package/dist/assets/runtimeEndpointReset-CjhvxLH0.js +0 -16
- package/dist/assets/scheduledTasksApi-DLW0mkQK.js +0 -1
- package/dist/assets/tr-DFnMlPxY.js +0 -6
- package/dist/assets/uk-ZrCNjM7Y.js +0 -15
- package/dist/assets/useAppFontEffects-CjrntdL0.js +0 -7310
- package/dist/assets/useInlineCommentController-CDEuSlPH.js +0 -2
- package/dist/assets/useRootScrollLock-BHYPkrLv.js +0 -1
- package/dist/assets/vendor-ghostty-web-CrxinGGH.js +0 -13
- package/dist/assets/vendor-zod-BLfBSgBh.js +0 -39
- package/dist/assets/zh-CN-iv_AV4EC.js +0 -15
- package/dist/assets/zh-TW-CzEUk38j.js +0 -15
- package/server/index.d.ts.bak +0 -39
- package/server/lib/agent-tool/DOCUMENTATION.md.bak +0 -119
- package/server/lib/browser-control/DOCUMENTATION.md.bak +0 -50
- package/server/lib/context-obligatory/DOCUMENTATION.md.bak +0 -19
- package/server/lib/dev-tunnel/DOCUMENTATION.md.bak +0 -57
- package/server/lib/dictation/DOCUMENTATION.md.bak +0 -100
- package/server/lib/event-stream/DOCUMENTATION.md.bak +0 -62
- package/server/lib/fs/DOCUMENTATION.md.bak +0 -47
- package/server/lib/git/DOCUMENTATION.md.bak +0 -193
- package/server/lib/github/DOCUMENTATION.md.bak +0 -183
- package/server/lib/linear/DOCUMENTATION.md.bak +0 -97
- package/server/lib/linear/OAUTH-BROKER-HANDOFF.md.bak +0 -91
- package/server/lib/markdown-image-grants/DOCUMENTATION.md.bak +0 -40
- package/server/lib/notifications/APNS.md.bak +0 -135
- package/server/lib/notifications/DOCUMENTATION.md.bak +0 -138
- package/server/lib/openchamber-control/DOCUMENTATION.md.bak +0 -64
- package/server/lib/opencode/DOCUMENTATION.md.bak +0 -431
- package/server/lib/opencode/managed-process-registry.d.ts.bak +0 -13
- package/server/lib/opencode/provider-env-aliases.d.ts.bak +0 -3
- package/server/lib/permission-auto-accept/DOCUMENTATION.md.bak +0 -34
- package/server/lib/project-context/DOCUMENTATION.md.bak +0 -160
- package/server/lib/quota/DOCUMENTATION.md.bak +0 -123
- package/server/lib/relay/DOCUMENTATION.md.bak +0 -100
- package/server/lib/scheduled-tasks/DOCUMENTATION.md.bak +0 -182
- package/server/lib/session-assist/DOCUMENTATION.md.bak +0 -66
- package/server/lib/session-goal/DOCUMENTATION.md.bak +0 -213
- package/server/lib/session-knowledge/DOCUMENTATION.md.bak +0 -89
- package/server/lib/skills-catalog/DOCUMENTATION.md.bak +0 -151
- package/server/lib/small-model/DOCUMENTATION.md.bak +0 -212
- package/server/lib/system-prompt/DOCUMENTATION.md.bak +0 -36
- package/server/lib/terminal/DOCUMENTATION.md.bak +0 -50
- package/server/lib/text/DOCUMENTATION.md.bak +0 -35
- package/server/lib/tts/DOCUMENTATION.md.bak +0 -147
- package/server/lib/tunnels/DOCUMENTATION.md.bak +0 -21
- package/server/lib/ui-auth/DOCUMENTATION.md.bak +0 -44
- package/server/lib/walkthrough/DOCUMENTATION.md.bak +0 -411
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { createDownstreamScheduler, DOWNSTREAM_CHUNK_BYTES } from './downstream-scheduler.js';
|
|
3
|
+
import { encodeTunnelFrame, decodeTunnelFrame, TunnelFrameType } from './tunnel-codec.js';
|
|
4
|
+
|
|
5
|
+
const tick = () => new Promise(resolve => setTimeout(resolve, 0));
|
|
6
|
+
const body = (streamId, value = 0, size = DOWNSTREAM_CHUNK_BYTES) =>
|
|
7
|
+
encodeTunnelFrame(TunnelFrameType.HttpBody, streamId, new Uint8Array(size).fill(value));
|
|
8
|
+
|
|
9
|
+
function fixture() {
|
|
10
|
+
const sent = [];
|
|
11
|
+
const errors = [];
|
|
12
|
+
let time = 0;
|
|
13
|
+
const scheduler = createDownstreamScheduler({
|
|
14
|
+
sendBatch: async frames => { sent.push(...frames); },
|
|
15
|
+
onError: error => errors.push(error.message),
|
|
16
|
+
now: () => time,
|
|
17
|
+
});
|
|
18
|
+
return { scheduler, sent, errors, advance: ms => { time += ms; } };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('relay downstream scheduling', () => {
|
|
22
|
+
test('stops without end-client ACKs even when the relay socket accepts everything', async () => {
|
|
23
|
+
const { scheduler, sent } = fixture();
|
|
24
|
+
const pending = Array.from({ length: 100 }, () => scheduler.send(body(1)));
|
|
25
|
+
await tick();
|
|
26
|
+
const bytes = sent.reduce((sum, frame) => sum + frame.length, 0);
|
|
27
|
+
expect(bytes).toBeLessThanOrEqual(64 * 1024);
|
|
28
|
+
expect(bytes).toBeGreaterThan(48 * 1024);
|
|
29
|
+
await tick();
|
|
30
|
+
expect(sent.reduce((sum, frame) => sum + frame.length, 0)).toBe(bytes);
|
|
31
|
+
scheduler.acknowledge(bytes);
|
|
32
|
+
await tick();
|
|
33
|
+
expect(sent.reduce((sum, frame) => sum + frame.length, 0)).toBeGreaterThan(bytes);
|
|
34
|
+
scheduler.close();
|
|
35
|
+
await Promise.all(pending);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('rotates streams while preserving every delta and each stream end', async () => {
|
|
39
|
+
const { scheduler, sent } = fixture();
|
|
40
|
+
const pending = [];
|
|
41
|
+
for (let value = 0; value < 3; value++) pending.push(scheduler.send(body(1, value)));
|
|
42
|
+
pending.push(scheduler.send(encodeTunnelFrame(TunnelFrameType.StreamEnd, 1, new Uint8Array())));
|
|
43
|
+
pending.push(scheduler.send(body(3, 99, 10)));
|
|
44
|
+
await Promise.all(pending);
|
|
45
|
+
expect(sent.map(frame => decodeTunnelFrame(frame).streamId)).toEqual([1, 3, 1, 1, 1]);
|
|
46
|
+
expect(sent.filter(frame => decodeTunnelFrame(frame).streamId === 1).map(frame => decodeTunnelFrame(frame).payload[0])).toEqual([0, 1, 2, undefined]);
|
|
47
|
+
scheduler.close();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('keepalives bypass credit; cancellation and close release blocked producers', async () => {
|
|
51
|
+
const { scheduler, sent } = fixture();
|
|
52
|
+
const pending = Array.from({ length: 20 }, () => scheduler.send(body(1)));
|
|
53
|
+
await tick();
|
|
54
|
+
const before = sent.length;
|
|
55
|
+
await scheduler.send(encodeTunnelFrame(TunnelFrameType.Pong, 0, new Uint8Array()));
|
|
56
|
+
expect(sent.length).toBe(before + 1);
|
|
57
|
+
scheduler.cancel(1);
|
|
58
|
+
await Promise.all(pending);
|
|
59
|
+
const blocked = scheduler.send(body(3));
|
|
60
|
+
scheduler.close();
|
|
61
|
+
await blocked;
|
|
62
|
+
// Teardown makes late ACKs harmless.
|
|
63
|
+
scheduler.acknowledge(123);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('rejects forged, replayed, regressing and partial-frame ACKs', async () => {
|
|
67
|
+
const { scheduler, sent } = fixture();
|
|
68
|
+
await scheduler.send(body(1));
|
|
69
|
+
const bytes = sent[0].length;
|
|
70
|
+
for (const value of [0, -1, 1, bytes + 1, NaN, Infinity]) {
|
|
71
|
+
expect(() => scheduler.acknowledge(value)).toThrow();
|
|
72
|
+
}
|
|
73
|
+
scheduler.acknowledge(bytes);
|
|
74
|
+
expect(() => scheduler.acknowledge(bytes)).toThrow();
|
|
75
|
+
scheduler.close();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('bounds pending memory and reports overflow instead of silently dropping deltas', async () => {
|
|
79
|
+
const { scheduler, errors } = fixture();
|
|
80
|
+
const pending = Array.from({ length: 600 }, () => scheduler.send(body(1)));
|
|
81
|
+
await Promise.all(pending);
|
|
82
|
+
expect(errors).toEqual(['relay downstream queue limit exceeded']);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('send failure settles selected and queued producers', async () => {
|
|
86
|
+
const errors = [];
|
|
87
|
+
const scheduler = createDownstreamScheduler({
|
|
88
|
+
sendBatch: async () => { throw new Error('wire failed'); },
|
|
89
|
+
onError: error => errors.push(error.message),
|
|
90
|
+
});
|
|
91
|
+
await Promise.all([scheduler.send(body(1)), scheduler.send(body(3))]);
|
|
92
|
+
expect(errors).toEqual(['wire failed']);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('teardown releases a producer even while encryption is still pending', async () => {
|
|
96
|
+
let release;
|
|
97
|
+
let started = false;
|
|
98
|
+
const scheduler = createDownstreamScheduler({
|
|
99
|
+
sendBatch: () => new Promise(resolve => { started = true; release = resolve; }),
|
|
100
|
+
onError: error => { throw error; },
|
|
101
|
+
});
|
|
102
|
+
const pending = scheduler.send(body(1));
|
|
103
|
+
await tick();
|
|
104
|
+
expect(started).toBe(true);
|
|
105
|
+
scheduler.close();
|
|
106
|
+
await pending;
|
|
107
|
+
release();
|
|
108
|
+
await tick();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('sparse traffic cannot inflate the window for a later bulk burst', async () => {
|
|
112
|
+
const { scheduler, sent, advance } = fixture();
|
|
113
|
+
let acknowledged = 0;
|
|
114
|
+
for (let i = 0; i < 100; i++) {
|
|
115
|
+
const frame = body(1);
|
|
116
|
+
await scheduler.send(frame);
|
|
117
|
+
advance(50);
|
|
118
|
+
acknowledged += frame.length;
|
|
119
|
+
scheduler.acknowledge(acknowledged);
|
|
120
|
+
}
|
|
121
|
+
const before = sent.length;
|
|
122
|
+
const pending = Array.from({ length: 100 }, () => scheduler.send(body(1)));
|
|
123
|
+
await tick();
|
|
124
|
+
expect(sent.slice(before).reduce((sum, frame) => sum + frame.length, 0)).toBeLessThanOrEqual(64 * 1024);
|
|
125
|
+
scheduler.close();
|
|
126
|
+
await Promise.all(pending);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('adapts to a clear high-RTT path, then contracts when delivery backs up', async () => {
|
|
130
|
+
const { scheduler, sent, advance, errors } = fixture();
|
|
131
|
+
let acknowledged = 0;
|
|
132
|
+
let maxFlight = 0;
|
|
133
|
+
const pending = Array.from({ length: 400 }, () => scheduler.send(body(1)));
|
|
134
|
+
for (let round = 0; round < 12; round++) {
|
|
135
|
+
// Keep enough queued output to exercise growth all the way to the cap.
|
|
136
|
+
while (pending.length < sent.length + 400) pending.push(scheduler.send(body(1)));
|
|
137
|
+
await tick();
|
|
138
|
+
const delivered = sent.reduce((sum, frame) => sum + frame.length, 0);
|
|
139
|
+
maxFlight = Math.max(maxFlight, delivered - acknowledged);
|
|
140
|
+
expect(delivered - acknowledged).toBeLessThanOrEqual(1024 * 1024);
|
|
141
|
+
advance(200);
|
|
142
|
+
scheduler.acknowledge(delivered);
|
|
143
|
+
acknowledged = delivered;
|
|
144
|
+
}
|
|
145
|
+
// Reach the cap with a backlogged sender, allowing for whole-frame sizing.
|
|
146
|
+
expect(maxFlight).toBeGreaterThan(1024 * 1024 - body(1).length);
|
|
147
|
+
// Keep the sender backlogged while ACK latency jumps to a second.
|
|
148
|
+
for (let round = 0; round < 12; round++) {
|
|
149
|
+
for (let i = 0; i < 30; i++) pending.push(scheduler.send(body(1)));
|
|
150
|
+
await tick();
|
|
151
|
+
const delivered = sent.reduce((sum, frame) => sum + frame.length, 0);
|
|
152
|
+
advance(1000);
|
|
153
|
+
scheduler.acknowledge(delivered);
|
|
154
|
+
acknowledged = delivered;
|
|
155
|
+
}
|
|
156
|
+
await tick();
|
|
157
|
+
expect(sent.reduce((sum, frame) => sum + frame.length, 0) - acknowledged).toBeLessThanOrEqual(64 * 1024);
|
|
158
|
+
expect(errors).toEqual([]);
|
|
159
|
+
scheduler.close();
|
|
160
|
+
await Promise.all(pending);
|
|
161
|
+
});
|
|
162
|
+
});
|
package/server/lib/relay/e2ee.js
CHANGED
|
@@ -252,11 +252,12 @@ const parseHandshakeMessage = (raw) => {
|
|
|
252
252
|
if (parsed.v !== RELAY_PROTOCOL_VERSION) return null;
|
|
253
253
|
// Unknown/missing capability flag = false = legacy behavior.
|
|
254
254
|
const batch = parsed.batch === true;
|
|
255
|
+
const flowControl = parsed.flowControl === true;
|
|
255
256
|
if (parsed.t === 'ready') {
|
|
256
|
-
return { t: 'ready', v: RELAY_PROTOCOL_VERSION, batch };
|
|
257
|
+
return { t: 'ready', v: RELAY_PROTOCOL_VERSION, batch, flowControl };
|
|
257
258
|
}
|
|
258
259
|
if (parsed.t === 'hello' && typeof parsed.nonce === 'string' && typeof parsed.clientPubJwk === 'object' && parsed.clientPubJwk !== null) {
|
|
259
|
-
return { t: 'hello', v: RELAY_PROTOCOL_VERSION, clientPubJwk: parsed.clientPubJwk, nonce: parsed.nonce, batch };
|
|
260
|
+
return { t: 'hello', v: RELAY_PROTOCOL_VERSION, clientPubJwk: parsed.clientPubJwk, nonce: parsed.nonce, batch, flowControl };
|
|
260
261
|
}
|
|
261
262
|
return null;
|
|
262
263
|
};
|
|
@@ -275,7 +276,7 @@ const failClosed = (reason) => ({
|
|
|
275
276
|
* { type: 'ignore' } — drop the frame
|
|
276
277
|
* { type: 'fail', closeCode, reason } — close the socket with closeCode
|
|
277
278
|
* @param {CryptoKey} hostEncPrivateKey long-lived ECDH private key
|
|
278
|
-
* @param {{ batch?: boolean }} [options]
|
|
279
|
+
* @param {{ batch?: boolean, flowControl?: boolean }} [options] Capabilities default true.
|
|
279
280
|
*/
|
|
280
281
|
export const createHostHandshake = (hostEncPrivateKey, options = {}) => {
|
|
281
282
|
const localBatch = options.batch !== false;
|
|
@@ -321,15 +322,16 @@ export const createHostHandshake = (hostEncPrivateKey, options = {}) => {
|
|
|
321
322
|
acceptedClientKeyFingerprint = fingerprint;
|
|
322
323
|
// Batching runs only if both peers advertised it.
|
|
323
324
|
negotiatedBatch = localBatch && message.batch === true;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
);
|
|
325
|
+
const flowControl = options.flowControl !== false && message.flowControl === true;
|
|
326
|
+
const ready = { t: 'ready', v: RELAY_PROTOCOL_VERSION };
|
|
327
|
+
if (negotiatedBatch) ready.batch = true;
|
|
328
|
+
if (flowControl) ready.flowControl = true;
|
|
329
|
+
readyText = JSON.stringify(ready);
|
|
329
330
|
established = true;
|
|
330
331
|
return {
|
|
331
332
|
type: 'established',
|
|
332
333
|
batch: negotiatedBatch,
|
|
334
|
+
flowControl,
|
|
333
335
|
replyText: readyText,
|
|
334
336
|
channel: {
|
|
335
337
|
encryptor: createFrameEncryptor(keys.hostToClient),
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import { WebSocketServer } from 'ws';
|
|
4
|
+
import { startRelayHost } from './host-client.js';
|
|
5
|
+
import { generateEcdhKeyPair, exportPublicKeyJwk } from './e2ee.js';
|
|
6
|
+
import { createRelayTunnelClient } from '../../../../ui/src/lib/relay/tunnel-client.ts';
|
|
7
|
+
|
|
8
|
+
const waitFor = async predicate => {
|
|
9
|
+
for (let attempt = 0; attempt < 1000; attempt++) {
|
|
10
|
+
if (predicate()) return;
|
|
11
|
+
await new Promise(resolve => setTimeout(resolve, 5));
|
|
12
|
+
}
|
|
13
|
+
throw new Error('test condition timed out');
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
async function exercise({ clientFlow = true, hostFlow = true, batch = true, cancel = false, websocket = false } = {}) {
|
|
17
|
+
const total = 512 * 1024;
|
|
18
|
+
const content = Buffer.alloc(total, 'a');
|
|
19
|
+
content.fill('b', total / 2);
|
|
20
|
+
const upstream = http.createServer((req, res) => {
|
|
21
|
+
// HTTP bearer ownership is unchanged by flow control.
|
|
22
|
+
if (req.headers.authorization !== 'Bearer fixture-token') { res.writeHead(401); res.end(); return; }
|
|
23
|
+
res.end(req.url === '/health' ? 'ok' : content);
|
|
24
|
+
});
|
|
25
|
+
const upstreamWs = new WebSocketServer({ noServer: true });
|
|
26
|
+
upstream.on('upgrade', (req, socket, head) => {
|
|
27
|
+
// Mirror the real URL-token and loopback-origin gates for tunneled WS.
|
|
28
|
+
const query = new URL(req.url, 'http://localhost');
|
|
29
|
+
if (query.searchParams.get('oc_url_token') !== 'fixture-url-token' || req.headers.origin !== `http://127.0.0.1:${upstream.address().port}`) {
|
|
30
|
+
socket.end('HTTP/1.1 403 Forbidden\r\n\r\n');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
upstreamWs.handleUpgrade(req, socket, head, ws => {
|
|
34
|
+
ws.send(content.subarray(0, total / 2));
|
|
35
|
+
ws.send(content.subarray(total / 2));
|
|
36
|
+
// Let Bun finish the local write turn, then close while the throttled
|
|
37
|
+
// tunnel still has most of the two messages queued.
|
|
38
|
+
setTimeout(() => ws.close(1000, 'complete'), 20);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
await new Promise(resolve => upstream.listen(0, '127.0.0.1', resolve));
|
|
42
|
+
const relay = new WebSocketServer({ host: '127.0.0.1', port: 0 });
|
|
43
|
+
await new Promise(resolve => relay.on('listening', resolve));
|
|
44
|
+
let control;
|
|
45
|
+
let clientSocket;
|
|
46
|
+
let hostData;
|
|
47
|
+
const waiting = [];
|
|
48
|
+
const down = [];
|
|
49
|
+
// This fixture brokers an isolated room only; it does not replace production
|
|
50
|
+
// relay authentication. Neither WebSocket leg fails or drops a frame.
|
|
51
|
+
relay.on('connection', (socket, req) => {
|
|
52
|
+
const role = new URL(req.url, 'http://localhost').searchParams.get('role');
|
|
53
|
+
if (role === 'host-control') {
|
|
54
|
+
control = socket;
|
|
55
|
+
socket.send(JSON.stringify({ type: 'sync', connectionIds: clientSocket ? ['fixture'] : [] }));
|
|
56
|
+
} else if (role === 'client') {
|
|
57
|
+
clientSocket = socket;
|
|
58
|
+
control?.send(JSON.stringify({ type: 'connected', connectionId: 'fixture' }));
|
|
59
|
+
socket.on('message', (data, binary) => {
|
|
60
|
+
if (hostData) hostData.send(data, { binary });
|
|
61
|
+
else waiting.push({ data, binary });
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
hostData = socket;
|
|
65
|
+
for (const frame of waiting.splice(0)) socket.send(frame.data, { binary: frame.binary });
|
|
66
|
+
socket.on('message', (data, binary) => {
|
|
67
|
+
down.push({ data, binary });
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
// Drain at a byte budget, not a frame count: frame-size changes cannot make
|
|
72
|
+
// the test's effective bandwidth change. Preserve whole WS messages in order.
|
|
73
|
+
let credit = 0;
|
|
74
|
+
const drain = setInterval(() => {
|
|
75
|
+
credit = Math.min(128 * 1024, credit + 4096);
|
|
76
|
+
while (down.length && down[0].data.length <= credit) {
|
|
77
|
+
const frame = down.shift();
|
|
78
|
+
credit -= frame.data.length;
|
|
79
|
+
clientSocket.send(frame.data, { binary: frame.binary });
|
|
80
|
+
}
|
|
81
|
+
}, 5);
|
|
82
|
+
const keys = await generateEcdhKeyPair();
|
|
83
|
+
const relayUrl = `ws://127.0.0.1:${relay.address().port}/ws`;
|
|
84
|
+
const host = startRelayHost({
|
|
85
|
+
relayUrl, localPort: upstream.address().port, batch, flowControl: hostFlow,
|
|
86
|
+
identity: { serverId: 'fixture', hostEncPrivateKey: keys.privateKey, signRelayAuth: () => ({ ts: 0, sig: '', pk: '' }) },
|
|
87
|
+
});
|
|
88
|
+
const client = createRelayTunnelClient({ relayUrl, serverId: 'fixture', hostEncPubJwk: await exportPublicKeyJwk(keys.publicKey), batch, flowControl: clientFlow });
|
|
89
|
+
const states = [];
|
|
90
|
+
client.subscribeStatus(status => states.push(status.state));
|
|
91
|
+
const headers = { authorization: 'Bearer fixture-token' };
|
|
92
|
+
let received = 0;
|
|
93
|
+
let finished = false;
|
|
94
|
+
let wsMessages = 0;
|
|
95
|
+
let streamFailure;
|
|
96
|
+
const chunks = [];
|
|
97
|
+
const abort = new AbortController();
|
|
98
|
+
try {
|
|
99
|
+
await waitFor(() => client.getStatus().state === 'connected');
|
|
100
|
+
let stream;
|
|
101
|
+
if (websocket) {
|
|
102
|
+
const socket = client.openWebSocket('/api/terminal/ws?oc_url_token=fixture-url-token');
|
|
103
|
+
stream = new Promise((resolve, reject) => {
|
|
104
|
+
socket.onmessage = event => {
|
|
105
|
+
const bytes = new Uint8Array(event.data);
|
|
106
|
+
chunks.push(bytes);
|
|
107
|
+
received += bytes.length;
|
|
108
|
+
wsMessages++;
|
|
109
|
+
};
|
|
110
|
+
socket.onclose = event => {
|
|
111
|
+
finished = true;
|
|
112
|
+
if (event.code !== 1000) reject(new Error(event.reason));
|
|
113
|
+
else resolve();
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
} else {
|
|
117
|
+
const response = await client.fetch('/api/global/event', { headers, signal: abort.signal });
|
|
118
|
+
stream = (async () => {
|
|
119
|
+
try {
|
|
120
|
+
for await (const chunk of response.body) { chunks.push(chunk); received += chunk.length; }
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (!cancel || error.name !== 'AbortError') throw error;
|
|
123
|
+
} finally { finished = true; }
|
|
124
|
+
})();
|
|
125
|
+
}
|
|
126
|
+
stream = stream.catch(error => { streamFailure = error; });
|
|
127
|
+
await waitFor(() => received > 0 || streamFailure);
|
|
128
|
+
if (streamFailure) throw streamFailure;
|
|
129
|
+
if (cancel) abort.abort();
|
|
130
|
+
const response = await client.fetch('/health', { headers, signal: AbortSignal.timeout(5000) });
|
|
131
|
+
expect(await response.text()).toBe('ok');
|
|
132
|
+
const bytesAtProbe = received;
|
|
133
|
+
await stream;
|
|
134
|
+
expect(streamFailure).toBeUndefined();
|
|
135
|
+
expect(finished).toBe(true);
|
|
136
|
+
if (!cancel) expect(Buffer.concat(chunks).equals(content)).toBe(true);
|
|
137
|
+
if (websocket) expect(wsMessages).toBe(2);
|
|
138
|
+
expect(states).toEqual(['connected']);
|
|
139
|
+
return { bytesAtProbe, total };
|
|
140
|
+
} finally {
|
|
141
|
+
client.close();
|
|
142
|
+
host.stop();
|
|
143
|
+
clearInterval(drain);
|
|
144
|
+
for (const ws of relay.clients) ws.terminate();
|
|
145
|
+
relay.close();
|
|
146
|
+
for (const ws of upstreamWs.clients) ws.terminate();
|
|
147
|
+
upstreamWs.close();
|
|
148
|
+
upstream.closeAllConnections();
|
|
149
|
+
upstream.close();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
describe('end-to-end downstream credit', () => {
|
|
154
|
+
test('small HTTP response overtakes bulk output without losing bytes or reconnecting', async () => {
|
|
155
|
+
const result = await exercise();
|
|
156
|
+
expect(result.bytesAtProbe).toBeLessThan(result.total / 2);
|
|
157
|
+
// Queue peaks depend on real ACK timing and include encryption overhead.
|
|
158
|
+
// downstream-scheduler.test.js checks credit bounds with a controlled clock.
|
|
159
|
+
});
|
|
160
|
+
test('works without batching', async () => {
|
|
161
|
+
const result = await exercise({ batch: false });
|
|
162
|
+
expect(result.bytesAtProbe).toBeLessThan(result.total / 2);
|
|
163
|
+
});
|
|
164
|
+
test('cancelling a blocked stream leaves unrelated requests usable', async () => {
|
|
165
|
+
const result = await exercise({ cancel: true });
|
|
166
|
+
expect(result.bytesAtProbe).toBeLessThan(result.total);
|
|
167
|
+
});
|
|
168
|
+
test('fragmented WS messages remain complete and precede the close', async () => {
|
|
169
|
+
await exercise({ websocket: true });
|
|
170
|
+
});
|
|
171
|
+
for (const legacy of [{ clientFlow: false }, { hostFlow: false }]) {
|
|
172
|
+
test(`legacy fallback ${JSON.stringify(legacy)} preserves delivery`, async () => {
|
|
173
|
+
const result = await exercise(legacy);
|
|
174
|
+
expect(result.bytesAtProbe).toBe(result.total);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
});
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
import { WebSocket } from 'ws';
|
|
7
7
|
|
|
8
8
|
import { RELAY_PROTOCOL_VERSION, RelayCloseCode, createHostHandshake } from './e2ee.js';
|
|
9
|
-
import { createOutboundFrameBatcher, decodeFrameBatch } from './tunnel-codec.js';
|
|
9
|
+
import { createOutboundFrameBatcher, decodeFrameBatch, decodeTunnelFrame, decodeDeliveryAck, encodeFrameBatch, TunnelFrameType } from './tunnel-codec.js';
|
|
10
10
|
import { createTunnelHost } from './tunnel-host.js';
|
|
11
|
+
import { createDownstreamScheduler, DOWNSTREAM_CHUNK_BYTES } from './downstream-scheduler.js';
|
|
11
12
|
|
|
12
13
|
const BACKOFF_BASE_MS = 1000;
|
|
13
14
|
const BACKOFF_CAP_MS = 30000;
|
|
@@ -33,7 +34,7 @@ const DEFAULT_BATCH_WINDOW_MS = 150;
|
|
|
33
34
|
// the 150 ms default. Only applies on directions where batching was negotiated.
|
|
34
35
|
const resolveBatchWindowMs = (option) => {
|
|
35
36
|
if (Number.isFinite(option) && option >= 0) return option;
|
|
36
|
-
const envValue = Number.parseInt(process.env.
|
|
37
|
+
const envValue = Number.parseInt(process.env.AIWORKER_RELAY_BATCH_WINDOW_MS ?? '', 10);
|
|
37
38
|
if (Number.isFinite(envValue) && envValue >= 0) return envValue;
|
|
38
39
|
return DEFAULT_BATCH_WINDOW_MS;
|
|
39
40
|
};
|
|
@@ -48,7 +49,7 @@ const resolveBatchWindowMs = (option) => {
|
|
|
48
49
|
* logger?: Pick<Console, 'warn'>,
|
|
49
50
|
* }} options
|
|
50
51
|
*/
|
|
51
|
-
export const startRelayHost = ({ relayUrl, identity, localPort, getLocalPort, onStatus, logger = console, batchWindowMs, batch }) => {
|
|
52
|
+
export const startRelayHost = ({ relayUrl, identity, localPort, getLocalPort, onStatus, logger = console, batchWindowMs, batch, flowControl }) => {
|
|
52
53
|
const resolveLocalPort = typeof getLocalPort === 'function' ? getLocalPort : () => localPort;
|
|
53
54
|
const localBatch = batch !== false;
|
|
54
55
|
const resolvedBatchWindowMs = resolveBatchWindowMs(batchWindowMs);
|
|
@@ -95,6 +96,7 @@ export const startRelayHost = ({ relayUrl, identity, localPort, getLocalPort, on
|
|
|
95
96
|
dataSockets.delete(connectionId);
|
|
96
97
|
if (entry.openTimer) clearTimeout(entry.openTimer);
|
|
97
98
|
entry.batcher?.dispose();
|
|
99
|
+
entry.scheduler?.close();
|
|
98
100
|
entry.tunnel?.close();
|
|
99
101
|
try {
|
|
100
102
|
if (entry.socket.readyState === WebSocket.OPEN || entry.socket.readyState === WebSocket.CONNECTING) {
|
|
@@ -118,14 +120,14 @@ export const startRelayHost = ({ relayUrl, identity, localPort, getLocalPort, on
|
|
|
118
120
|
return;
|
|
119
121
|
}
|
|
120
122
|
|
|
121
|
-
const entry = { socket, tunnel: null, openTimer: null, batcher: null, lastActivityAt: Date.now() };
|
|
123
|
+
const entry = { socket, tunnel: null, openTimer: null, batcher: null, scheduler: null, lastActivityAt: Date.now() };
|
|
122
124
|
dataSockets.set(connectionId, entry);
|
|
123
125
|
entry.openTimer = setTimeout(() => {
|
|
124
126
|
logger.warn('[Relay] host-data socket open timeout');
|
|
125
127
|
teardownDataSocket(connectionId);
|
|
126
128
|
}, DATA_SOCKET_OPEN_TIMEOUT_MS);
|
|
127
129
|
|
|
128
|
-
const handshake = createHostHandshake(identity.hostEncPrivateKey, { batch: localBatch });
|
|
130
|
+
const handshake = createHostHandshake(identity.hostEncPrivateKey, { batch: localBatch, flowControl });
|
|
129
131
|
let channel = null;
|
|
130
132
|
let batchNegotiated = false;
|
|
131
133
|
// Serialize async message handling so encrypted frame order (and the
|
|
@@ -140,11 +142,14 @@ export const startRelayHost = ({ relayUrl, identity, localPort, getLocalPort, on
|
|
|
140
142
|
.then(async () => {
|
|
141
143
|
if (dataSockets.get(connectionId) !== entry || socket.readyState !== WebSocket.OPEN || !channel) return;
|
|
142
144
|
const encrypted = await channel.encryptor.encrypt(plaintext);
|
|
145
|
+
if (dataSockets.get(connectionId) !== entry || socket.readyState !== WebSocket.OPEN) return;
|
|
143
146
|
socket.send(encrypted, { binary: true });
|
|
144
147
|
})
|
|
145
148
|
.catch((error) => {
|
|
146
149
|
logger.warn(`[Relay] host-data send failed: ${error?.message ?? error}`);
|
|
150
|
+
failChannel(RelayCloseCode.ChannelFailure, 'send failed');
|
|
147
151
|
});
|
|
152
|
+
return sendChain;
|
|
148
153
|
};
|
|
149
154
|
|
|
150
155
|
const failChannel = (closeCode, reason) => {
|
|
@@ -167,17 +172,25 @@ export const startRelayHost = ({ relayUrl, identity, localPort, getLocalPort, on
|
|
|
167
172
|
} else if (action.type === 'established') {
|
|
168
173
|
channel = action.channel;
|
|
169
174
|
batchNegotiated = action.batch === true;
|
|
170
|
-
entry.
|
|
175
|
+
entry.scheduler = action.flowControl ? createDownstreamScheduler({
|
|
176
|
+
sendBatch: frames => sendEncryptedPlaintext(batchNegotiated ? encodeFrameBatch(frames) : frames[0]),
|
|
177
|
+
maxBatchFrames: batchNegotiated ? 32 : 1,
|
|
178
|
+
onError: () => failChannel(RelayCloseCode.ChannelFailure, 'downstream queue failed'),
|
|
179
|
+
}) : null;
|
|
180
|
+
entry.batcher = batchNegotiated && !entry.scheduler
|
|
171
181
|
? createOutboundFrameBatcher({ windowMs: resolvedBatchWindowMs, sendBatch: sendEncryptedPlaintext })
|
|
172
182
|
: null;
|
|
173
183
|
entry.tunnel = createTunnelHost({
|
|
174
184
|
connectionId,
|
|
175
185
|
getLocalPort: resolveLocalPort,
|
|
176
186
|
getBufferedAmount: () => socket.bufferedAmount,
|
|
187
|
+
responseChunkBytes: entry.scheduler ? DOWNSTREAM_CHUNK_BYTES : undefined,
|
|
188
|
+
cancelPendingFrames: streamId => entry.scheduler?.cancel(streamId),
|
|
177
189
|
sendFrame: (plaintextFrame) => {
|
|
178
190
|
if (dataSockets.get(connectionId) !== entry || socket.readyState !== WebSocket.OPEN) return;
|
|
191
|
+
if (entry.scheduler) return entry.scheduler.send(plaintextFrame);
|
|
179
192
|
if (entry.batcher) entry.batcher.enqueue(plaintextFrame);
|
|
180
|
-
else sendEncryptedPlaintext(plaintextFrame);
|
|
193
|
+
else return sendEncryptedPlaintext(plaintextFrame);
|
|
181
194
|
},
|
|
182
195
|
});
|
|
183
196
|
if (action.replyText) socket.send(action.replyText);
|
|
@@ -205,16 +218,32 @@ export const startRelayHost = ({ relayUrl, identity, localPort, getLocalPort, on
|
|
|
205
218
|
// in order through the same per-frame handling as legacy.
|
|
206
219
|
for (const frame of decodeFrameBatch(plaintext)) {
|
|
207
220
|
if (dataSockets.get(connectionId) !== entry) return;
|
|
208
|
-
await
|
|
221
|
+
await dispatchFrame(frame);
|
|
209
222
|
}
|
|
210
223
|
} else {
|
|
211
|
-
await
|
|
224
|
+
await dispatchFrame(plaintext);
|
|
212
225
|
}
|
|
213
226
|
} catch (error) {
|
|
214
227
|
logger.warn(`[Relay] tunnel frame handling failed: ${error?.message ?? error}`);
|
|
228
|
+
failChannel(RelayCloseCode.ChannelFailure, 'invalid tunnel frame');
|
|
215
229
|
}
|
|
216
230
|
};
|
|
217
231
|
|
|
232
|
+
const dispatchFrame = (plaintext) => {
|
|
233
|
+
const frame = decodeTunnelFrame(plaintext);
|
|
234
|
+
if (frame.frameType === TunnelFrameType.DeliveryAck) {
|
|
235
|
+
if (!entry.scheduler || frame.streamId !== 0 || frame.hasMoreFragments) {
|
|
236
|
+
throw new Error('unexpected delivery acknowledgement');
|
|
237
|
+
}
|
|
238
|
+
entry.scheduler.acknowledge(decodeDeliveryAck(frame.payload));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
// Never await outbound credit on the receive chain: ACKs use this chain too.
|
|
242
|
+
void entry.tunnel.handleFrame(plaintext).catch(() => {
|
|
243
|
+
failChannel(RelayCloseCode.ChannelFailure, 'invalid tunnel frame');
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
|
|
218
247
|
socket.on('open', () => {
|
|
219
248
|
if (entry.openTimer) {
|
|
220
249
|
clearTimeout(entry.openTimer);
|
|
@@ -98,6 +98,9 @@ const startFakeRelay = () => {
|
|
|
98
98
|
wsUrl: `ws://127.0.0.1:${port}`,
|
|
99
99
|
state,
|
|
100
100
|
stop: () => new Promise((r) => {
|
|
101
|
+
// A socket a failed test left open would hold server.close() until
|
|
102
|
+
// the hook timeout; drop them so a failure is reported once.
|
|
103
|
+
for (const client of wss.clients) client.terminate();
|
|
101
104
|
wss.close();
|
|
102
105
|
server.close(() => r());
|
|
103
106
|
}),
|
|
@@ -162,7 +165,6 @@ const runScriptedClient = async ({ relayUrl, serverId, hostEncPubJwk }) => {
|
|
|
162
165
|
url.searchParams.set('role', 'client');
|
|
163
166
|
url.searchParams.set('serverId', serverId);
|
|
164
167
|
url.searchParams.set('connectionId', connectionId);
|
|
165
|
-
const ws = new WebSocket(url.toString());
|
|
166
168
|
|
|
167
169
|
const hostPub = await globalThis.crypto.subtle.importKey(
|
|
168
170
|
'jwk',
|
|
@@ -182,6 +184,12 @@ const runScriptedClient = async ({ relayUrl, serverId, hostEncPubJwk }) => {
|
|
|
182
184
|
resolveDone = resolve;
|
|
183
185
|
});
|
|
184
186
|
|
|
187
|
+
// Dialed only now, with the key material ready and the listeners attached
|
|
188
|
+
// in the same tick. Dialing before the WebCrypto awaits above let a loopback
|
|
189
|
+
// socket open while the key generation was still queued on the threadpool,
|
|
190
|
+
// and an `open` event with no listener means no hello, no ready, and a
|
|
191
|
+
// client that waits forever. Loaded CI runners hit exactly that.
|
|
192
|
+
const ws = new WebSocket(url.toString());
|
|
185
193
|
ws.on('open', async () => {
|
|
186
194
|
ws.send(JSON.stringify({
|
|
187
195
|
t: 'hello',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Per-machine relay-host claim. Every
|
|
1
|
+
// Per-machine relay-host claim. Every AiWorker instance on a machine shares
|
|
2
2
|
// the same data dir and therefore the same relay signing key / serverId, so if
|
|
3
3
|
// two processes run a relay host at once they fight over the single host slot
|
|
4
4
|
// at the relay worker (each new connection closes the previous one with
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Config lives in the server settings file as `settings.privateRelay =
|
|
5
5
|
// { enabled, relayUrl }` (same storage precedent as tunnels/notifications).
|
|
6
|
-
// Routes are registered with the other
|
|
6
|
+
// Routes are registered with the other AiWorker feature routes, before the
|
|
7
7
|
// generic OpenCode proxy, and are covered by the same global UI auth gate.
|
|
8
8
|
//
|
|
9
9
|
// Cross-runtime parity note: relay host mode intentionally targets the web
|
|
@@ -16,7 +16,7 @@ import express from 'express';
|
|
|
16
16
|
import { createRelayIdentityRuntime } from './identity.js';
|
|
17
17
|
import { startRelayHost } from './host-client.js';
|
|
18
18
|
|
|
19
|
-
export const DEFAULT_RELAY_URL = 'wss://relay.
|
|
19
|
+
export const DEFAULT_RELAY_URL = 'wss://relay.aiworker.dev/ws';
|
|
20
20
|
|
|
21
21
|
const isValidRelayUrl = (value) => {
|
|
22
22
|
if (typeof value !== 'string') return false;
|
|
@@ -40,7 +40,7 @@ const normalizeRelayUrl = (value) => {
|
|
|
40
40
|
// stored setting entirely, so the host connection, the pairing offer, and the
|
|
41
41
|
// status all point at it — clients then inherit it from the offer automatically.
|
|
42
42
|
const envRelayUrlOverride = () => {
|
|
43
|
-
const raw = process.env.
|
|
43
|
+
const raw = process.env.AIWORKER_RELAY_URL;
|
|
44
44
|
if (typeof raw !== 'string' || !raw.trim() || !isValidRelayUrl(raw)) return null;
|
|
45
45
|
return raw.trim();
|
|
46
46
|
};
|
|
@@ -100,7 +100,7 @@ export const createRelayService = ({
|
|
|
100
100
|
return {
|
|
101
101
|
enabled: stored?.enabled === true,
|
|
102
102
|
relayUrl: override ?? normalizeRelayUrl(stored?.relayUrl),
|
|
103
|
-
// True when the endpoint is pinned by
|
|
103
|
+
// True when the endpoint is pinned by AIWORKER_RELAY_URL (a self-hosted
|
|
104
104
|
// relay); the stored setting is ignored while it is set.
|
|
105
105
|
relayUrlLocked: override !== null,
|
|
106
106
|
};
|
|
@@ -122,7 +122,7 @@ export const createRelayService = ({
|
|
|
122
122
|
|
|
123
123
|
const standbyStatus = (holderPid) => ({
|
|
124
124
|
state: 'standby',
|
|
125
|
-
lastError: `relay host is owned by another local
|
|
125
|
+
lastError: `relay host is owned by another local AiWorker process (pid ${holderPid})`,
|
|
126
126
|
connectedClients: 0,
|
|
127
127
|
});
|
|
128
128
|
|
|
@@ -34,6 +34,7 @@ export const TunnelFrameType = {
|
|
|
34
34
|
WsClose: 10,
|
|
35
35
|
Ping: 11,
|
|
36
36
|
Pong: 12,
|
|
37
|
+
DeliveryAck: 13,
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
const TUNNEL_FRAME_TYPE_VALUES = new Set(Object.values(TunnelFrameType));
|
|
@@ -43,6 +44,14 @@ export const isTunnelFrameType = (value) => TUNNEL_FRAME_TYPE_VALUES.has(value);
|
|
|
43
44
|
|
|
44
45
|
const MAX_STREAM_ID = 0xffffffff;
|
|
45
46
|
|
|
47
|
+
/** Decode the client's cumulative raw tunnel-frame byte count. */
|
|
48
|
+
export const decodeDeliveryAck = (payload) => {
|
|
49
|
+
if (payload.length !== 8) throw new Error('invalid delivery acknowledgement');
|
|
50
|
+
const bytes = Number(new DataView(payload.buffer, payload.byteOffset, payload.byteLength).getBigUint64(0));
|
|
51
|
+
if (!Number.isSafeInteger(bytes)) throw new Error('invalid delivery acknowledgement');
|
|
52
|
+
return bytes;
|
|
53
|
+
};
|
|
54
|
+
|
|
46
55
|
export class TunnelCodecError extends Error {
|
|
47
56
|
constructor(message) {
|
|
48
57
|
super(message);
|