@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
|
@@ -50,11 +50,11 @@ describe('Linear session status runtime', () => {
|
|
|
50
50
|
let previousPort;
|
|
51
51
|
|
|
52
52
|
beforeEach(() => {
|
|
53
|
-
previousDataDir = process.env.
|
|
54
|
-
previousPort = process.env.
|
|
53
|
+
previousDataDir = process.env.AIWORKER_DATA_DIR;
|
|
54
|
+
previousPort = process.env.AIWORKER_PORT;
|
|
55
55
|
dataDir = makeTempDir();
|
|
56
|
-
process.env.
|
|
57
|
-
process.env.
|
|
56
|
+
process.env.AIWORKER_DATA_DIR = dataDir;
|
|
57
|
+
process.env.AIWORKER_PORT = '3001';
|
|
58
58
|
setLinearAuth({
|
|
59
59
|
accessToken: 'access-1',
|
|
60
60
|
refreshToken: 'refresh-1',
|
|
@@ -69,14 +69,14 @@ describe('Linear session status runtime', () => {
|
|
|
69
69
|
vi.unstubAllGlobals();
|
|
70
70
|
clearLinearAuth();
|
|
71
71
|
if (previousDataDir === undefined) {
|
|
72
|
-
delete process.env.
|
|
72
|
+
delete process.env.AIWORKER_DATA_DIR;
|
|
73
73
|
} else {
|
|
74
|
-
process.env.
|
|
74
|
+
process.env.AIWORKER_DATA_DIR = previousDataDir;
|
|
75
75
|
}
|
|
76
76
|
if (previousPort === undefined) {
|
|
77
|
-
delete process.env.
|
|
77
|
+
delete process.env.AIWORKER_PORT;
|
|
78
78
|
} else {
|
|
79
|
-
process.env.
|
|
79
|
+
process.env.AIWORKER_PORT = previousPort;
|
|
80
80
|
}
|
|
81
81
|
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
82
82
|
});
|
|
@@ -147,7 +147,7 @@ describe('Linear session status runtime', () => {
|
|
|
147
147
|
});
|
|
148
148
|
expect(commentCalls).toHaveLength(1);
|
|
149
149
|
const body = JSON.parse(commentCalls[0][1].body).variables.input.body;
|
|
150
|
-
expect(body).toContain('
|
|
150
|
+
expect(body).toContain('AiWorker session failed');
|
|
151
151
|
});
|
|
152
152
|
runtime.stop();
|
|
153
153
|
});
|
|
@@ -117,7 +117,7 @@ function statusWord(kind) {
|
|
|
117
117
|
|
|
118
118
|
export function buildLinearSessionStatusComment({ kind, sessionUrl }) {
|
|
119
119
|
const url = readTrimmedString(sessionUrl);
|
|
120
|
-
const label = `
|
|
120
|
+
const label = `AiWorker session ${statusWord(kind)}`;
|
|
121
121
|
if (!url) return label;
|
|
122
122
|
// The comment already lives on the issue, so it says only what happened and
|
|
123
123
|
// links to the session. Issue titles routinely contain brackets ("[Bug] …"),
|
|
@@ -59,11 +59,11 @@ describe('Linear session status comments', () => {
|
|
|
59
59
|
let previousPort;
|
|
60
60
|
|
|
61
61
|
beforeEach(() => {
|
|
62
|
-
previousDataDir = process.env.
|
|
63
|
-
previousPort = process.env.
|
|
62
|
+
previousDataDir = process.env.AIWORKER_DATA_DIR;
|
|
63
|
+
previousPort = process.env.AIWORKER_PORT;
|
|
64
64
|
dataDir = makeTempDir();
|
|
65
|
-
process.env.
|
|
66
|
-
process.env.
|
|
65
|
+
process.env.AIWORKER_DATA_DIR = dataDir;
|
|
66
|
+
process.env.AIWORKER_PORT = '3001';
|
|
67
67
|
setLinearAuth({
|
|
68
68
|
accessToken: 'access-1',
|
|
69
69
|
refreshToken: 'refresh-1',
|
|
@@ -78,14 +78,14 @@ describe('Linear session status comments', () => {
|
|
|
78
78
|
vi.unstubAllGlobals();
|
|
79
79
|
clearLinearAuth();
|
|
80
80
|
if (previousDataDir === undefined) {
|
|
81
|
-
delete process.env.
|
|
81
|
+
delete process.env.AIWORKER_DATA_DIR;
|
|
82
82
|
} else {
|
|
83
|
-
process.env.
|
|
83
|
+
process.env.AIWORKER_DATA_DIR = previousDataDir;
|
|
84
84
|
}
|
|
85
85
|
if (previousPort === undefined) {
|
|
86
|
-
delete process.env.
|
|
86
|
+
delete process.env.AIWORKER_PORT;
|
|
87
87
|
} else {
|
|
88
|
-
process.env.
|
|
88
|
+
process.env.AIWORKER_PORT = previousPort;
|
|
89
89
|
}
|
|
90
90
|
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
91
91
|
});
|
|
@@ -95,7 +95,7 @@ describe('Linear session status comments', () => {
|
|
|
95
95
|
expect(readSessionOrigin('http://127.0.0.1:3001/')).toBe('http://127.0.0.1:3001');
|
|
96
96
|
expect(readSessionOrigin('javascript:alert(1)')).toBe('');
|
|
97
97
|
expect(readSessionOrigin('https://app.example.com/secret')).toBe('');
|
|
98
|
-
expect(readSessionOrigin('
|
|
98
|
+
expect(readSessionOrigin('aiworker:')).toBe('');
|
|
99
99
|
expect(buildLinearSessionOpenUrl('ses_1', 'https://app.example.com'))
|
|
100
100
|
.toBe('https://app.example.com/?session=ses_1');
|
|
101
101
|
expect(buildLinearSessionOpenUrl('ses_1', '')).toBe('');
|
|
@@ -117,7 +117,7 @@ describe('Linear session status comments', () => {
|
|
|
117
117
|
expect(isPublicSessionOrigin('http://macbook.local:3001')).toBe(false);
|
|
118
118
|
expect(isPublicSessionOrigin('http://macbook:3001')).toBe(false);
|
|
119
119
|
expect(isPublicSessionOrigin('http://[fd00::1]:3001')).toBe(false);
|
|
120
|
-
expect(isPublicSessionOrigin('
|
|
120
|
+
expect(isPublicSessionOrigin('aiworker:')).toBe(false);
|
|
121
121
|
expect(isPublicSessionOrigin('')).toBe(false);
|
|
122
122
|
});
|
|
123
123
|
|
|
@@ -164,15 +164,15 @@ describe('Linear session status comments', () => {
|
|
|
164
164
|
expect(buildLinearSessionStatusComment({
|
|
165
165
|
kind: 'started',
|
|
166
166
|
sessionUrl: 'https://app.example.com/?session=ses_1',
|
|
167
|
-
})).toBe('[
|
|
167
|
+
})).toBe('[AiWorker session started](https://app.example.com/?session=ses_1)');
|
|
168
168
|
expect(buildLinearSessionStatusComment({
|
|
169
169
|
kind: 'completed',
|
|
170
170
|
sessionUrl: 'https://app.example.com/?session=ses_1',
|
|
171
|
-
})).toBe('[
|
|
171
|
+
})).toBe('[AiWorker session completed](https://app.example.com/?session=ses_1)');
|
|
172
172
|
expect(buildLinearSessionStatusComment({
|
|
173
173
|
kind: 'failure',
|
|
174
174
|
sessionUrl: 'https://app.example.com/?session=ses_1',
|
|
175
|
-
})).toBe('[
|
|
175
|
+
})).toBe('[AiWorker session failed](https://app.example.com/?session=ses_1)');
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
it('cannot be broken by brackets in the issue title', async () => {
|
|
@@ -231,7 +231,7 @@ describe('Linear session status comments', () => {
|
|
|
231
231
|
});
|
|
232
232
|
expect(commentCalls).toHaveLength(1);
|
|
233
233
|
const body = JSON.parse(commentCalls[0][1].body).variables.input.body;
|
|
234
|
-
expect(body).toBe('[
|
|
234
|
+
expect(body).toBe('[AiWorker session started](https://app.example.com/?session=ses_1)');
|
|
235
235
|
expect(JSON.stringify(first)).not.toContain('access-1');
|
|
236
236
|
});
|
|
237
237
|
|
|
@@ -266,6 +266,6 @@ describe('Linear session status comments', () => {
|
|
|
266
266
|
});
|
|
267
267
|
expect(commentCalls).toHaveLength(1);
|
|
268
268
|
const body = JSON.parse(commentCalls[0][1].body).variables.input.body;
|
|
269
|
-
expect(body).toBe('[
|
|
269
|
+
expect(body).toBe('[AiWorker session completed](https://app.example.com/?session=ses_1)');
|
|
270
270
|
});
|
|
271
271
|
});
|
|
@@ -17,9 +17,9 @@ describe('Linear teams list', () => {
|
|
|
17
17
|
let previousDataDir;
|
|
18
18
|
|
|
19
19
|
beforeEach(() => {
|
|
20
|
-
previousDataDir = process.env.
|
|
20
|
+
previousDataDir = process.env.AIWORKER_DATA_DIR;
|
|
21
21
|
dataDir = makeTempDir();
|
|
22
|
-
process.env.
|
|
22
|
+
process.env.AIWORKER_DATA_DIR = dataDir;
|
|
23
23
|
setLinearAuth({
|
|
24
24
|
accessToken: 'access-1',
|
|
25
25
|
refreshToken: 'refresh-1',
|
|
@@ -33,9 +33,9 @@ describe('Linear teams list', () => {
|
|
|
33
33
|
vi.unstubAllGlobals();
|
|
34
34
|
clearLinearAuth();
|
|
35
35
|
if (previousDataDir === undefined) {
|
|
36
|
-
delete process.env.
|
|
36
|
+
delete process.env.AIWORKER_DATA_DIR;
|
|
37
37
|
} else {
|
|
38
|
-
process.env.
|
|
38
|
+
process.env.AIWORKER_DATA_DIR = previousDataDir;
|
|
39
39
|
}
|
|
40
40
|
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
41
41
|
});
|
|
@@ -4,13 +4,13 @@ export const registerMagicPromptRoutes = (app, dependencies) => {
|
|
|
4
4
|
const {
|
|
5
5
|
fsPromises,
|
|
6
6
|
path,
|
|
7
|
-
|
|
7
|
+
aiworkerDataDir,
|
|
8
8
|
} = dependencies;
|
|
9
9
|
|
|
10
10
|
const runtime = createMagicPromptRuntime({
|
|
11
11
|
fsPromises,
|
|
12
12
|
path,
|
|
13
|
-
filePath: path.join(
|
|
13
|
+
filePath: path.join(aiworkerDataDir, 'magic-prompts.json'),
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
app.get('/api/magic-prompts', async (_req, res) => {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Managed MCP Reconnect
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
OpenCode connects each configured MCP server once, when a project directory is
|
|
6
|
+
first used. A server that does not come up then is marked `failed` and never
|
|
7
|
+
retried; a server whose live connection later drops is marked `failed` too and
|
|
8
|
+
stays that way until OpenCode restarts. This module injects a small plugin into
|
|
9
|
+
the OpenCode process AiWorker launches that reconnects those servers, so a
|
|
10
|
+
server that was slow to start or crashed mid-session comes back on its own.
|
|
11
|
+
|
|
12
|
+
## Runtime flow
|
|
13
|
+
|
|
14
|
+
1. `prepareManagedOpenCodeEnv(configContent)` materializes the plugin under
|
|
15
|
+
`<openchamber-data-dir>/mcp-reconnect/` and appends its `file://` URL to
|
|
16
|
+
`OPENCODE_CONFIG_CONTENT` through the shared merge in
|
|
17
|
+
`packages/web/server/lib/opencode/managed-plugin-config.js`.
|
|
18
|
+
2. It is always on for managed OpenCode. There is no setting, because it only
|
|
19
|
+
acts on servers OpenCode has already given up on.
|
|
20
|
+
3. OpenCode loads the plugin once per project directory with an SDK client
|
|
21
|
+
scoped to that directory, so each directory reconnects its own servers.
|
|
22
|
+
4. The plugin reads MCP status one second after load, calls connect for every
|
|
23
|
+
server in the `failed` state, then re-reads status after a per-server delay
|
|
24
|
+
that doubles from one second to a cap of thirty. A server seen in any other
|
|
25
|
+
state resets its counter. While nothing is failed it checks every thirty
|
|
26
|
+
seconds.
|
|
27
|
+
5. A dropped connection publishes `mcp.tools.changed`, which the plugin uses to
|
|
28
|
+
check right away instead of waiting out the idle interval.
|
|
29
|
+
6. OpenCode calls the plugin's `dispose` hook when it tears the directory down,
|
|
30
|
+
which stops the loop.
|
|
31
|
+
|
|
32
|
+
## Invariants
|
|
33
|
+
|
|
34
|
+
- Only `failed` is retried. `disabled` is the user's choice, and `needs_auth`
|
|
35
|
+
or `needs_client_registration` need the user to act; retrying those would
|
|
36
|
+
either re-enable a server the user turned off or loop on a login prompt.
|
|
37
|
+
- The plugin logs nothing and swallows every error. OpenCode already logs each
|
|
38
|
+
failed attempt, and a status call failing during an OpenCode restart is not
|
|
39
|
+
news.
|
|
40
|
+
- One check runs at a time; a wake-up arriving during a check is honored once
|
|
41
|
+
it finishes rather than starting a second loop.
|
|
42
|
+
|
|
43
|
+
## What the UI sees
|
|
44
|
+
|
|
45
|
+
OpenCode publishes no event when a reconnect succeeds. The chat picks the
|
|
46
|
+
server up on the next prompt because tools are resolved from live state, but
|
|
47
|
+
the MCP page reads status on bootstrap and refresh, so it can show `failed` for
|
|
48
|
+
a while after the server is back.
|
|
49
|
+
|
|
50
|
+
## Runtime parity
|
|
51
|
+
|
|
52
|
+
- Web and Desktop managed OpenCode: injected automatically.
|
|
53
|
+
- External OpenCode (`OPENCODE_HOST` or skip-start) and VS Code's separate
|
|
54
|
+
OpenCode lifecycle: not injected, because AiWorker does not control that
|
|
55
|
+
process environment.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
2
|
+
import { appendManagedPlugin } from '../opencode/managed-plugin-config.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* OpenCode marks an MCP server `failed` when it does not come up at startup or
|
|
6
|
+
* when a live connection drops, and never tries again. This plugin runs inside
|
|
7
|
+
* the managed OpenCode process and reconnects those servers with a per-server
|
|
8
|
+
* exponential backoff, so a server that was merely slow to start, or a local
|
|
9
|
+
* one that crashed, comes back without an OpenCode restart.
|
|
10
|
+
*
|
|
11
|
+
* Only `failed` is retried. `needs_auth`, `needs_client_registration`, and
|
|
12
|
+
* `disabled` are user decisions or need user action, and retrying them would
|
|
13
|
+
* either loop on a login prompt or re-enable a server the user turned off.
|
|
14
|
+
*
|
|
15
|
+
* The plugin talks to OpenCode through the SDK client OpenCode hands it, which
|
|
16
|
+
* is scoped to one project directory, so each open directory reconnects its
|
|
17
|
+
* own servers. Nothing is logged: OpenCode already reports each failed attempt.
|
|
18
|
+
*/
|
|
19
|
+
const createPluginSource = () => String.raw`
|
|
20
|
+
const INITIAL_RETRY_MS = 1000
|
|
21
|
+
const MAX_RETRY_MS = 30000
|
|
22
|
+
const IDLE_CHECK_MS = 30000
|
|
23
|
+
|
|
24
|
+
export const AiWorkerMcpReconnectPlugin = async ({ client }) => {
|
|
25
|
+
let disposed = false
|
|
26
|
+
let running = false
|
|
27
|
+
let wakeRequested = false
|
|
28
|
+
let timer
|
|
29
|
+
// Consecutive failed attempts per server, cleared once it is seen healthy.
|
|
30
|
+
const attempts = new Map()
|
|
31
|
+
const dueAt = new Map()
|
|
32
|
+
|
|
33
|
+
const schedule = (delayMs) => {
|
|
34
|
+
if (disposed) return
|
|
35
|
+
clearTimeout(timer)
|
|
36
|
+
timer = setTimeout(tick, delayMs)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const tick = async () => {
|
|
40
|
+
if (running) {
|
|
41
|
+
wakeRequested = true
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
running = true
|
|
45
|
+
let delay = IDLE_CHECK_MS
|
|
46
|
+
try {
|
|
47
|
+
const statuses = (await client.mcp.status())?.data ?? {}
|
|
48
|
+
const now = Date.now()
|
|
49
|
+
const due = []
|
|
50
|
+
for (const [name, entry] of Object.entries(statuses)) {
|
|
51
|
+
if (entry?.status !== "failed") {
|
|
52
|
+
attempts.delete(name)
|
|
53
|
+
dueAt.delete(name)
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
const at = dueAt.get(name) ?? now
|
|
57
|
+
if (at > now) {
|
|
58
|
+
delay = Math.min(delay, at - now)
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
due.push(name)
|
|
62
|
+
}
|
|
63
|
+
for (const name of attempts.keys()) {
|
|
64
|
+
if (!Object.hasOwn(statuses, name)) {
|
|
65
|
+
attempts.delete(name)
|
|
66
|
+
dueAt.delete(name)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await Promise.allSettled(due.map((name) => client.mcp.connect({ path: { name } })))
|
|
71
|
+
|
|
72
|
+
// The result is read on the next tick: a server that came back clears
|
|
73
|
+
// its counter there, one still failed waits out its backoff.
|
|
74
|
+
const after = Date.now()
|
|
75
|
+
for (const name of due) {
|
|
76
|
+
const count = (attempts.get(name) ?? 0) + 1
|
|
77
|
+
const wait = Math.min(INITIAL_RETRY_MS * 2 ** (count - 1), MAX_RETRY_MS)
|
|
78
|
+
attempts.set(name, count)
|
|
79
|
+
dueAt.set(name, after + wait)
|
|
80
|
+
delay = Math.min(delay, wait)
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// Status is unavailable while OpenCode is shutting down or restarting;
|
|
84
|
+
// the idle check picks up again when it is back.
|
|
85
|
+
} finally {
|
|
86
|
+
running = false
|
|
87
|
+
const wake = wakeRequested
|
|
88
|
+
wakeRequested = false
|
|
89
|
+
schedule(wake ? INITIAL_RETRY_MS : delay)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
schedule(INITIAL_RETRY_MS)
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
// A dropped connection publishes this event; checking right away beats
|
|
97
|
+
// waiting out the idle interval.
|
|
98
|
+
event: async ({ event }) => {
|
|
99
|
+
if (event?.type === "mcp.tools.changed") schedule(INITIAL_RETRY_MS)
|
|
100
|
+
},
|
|
101
|
+
dispose: async () => {
|
|
102
|
+
disposed = true
|
|
103
|
+
clearTimeout(timer)
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
export const createMcpReconnectRuntime = ({ fsPromises, path, dataDir }) => {
|
|
110
|
+
const pluginDirectory = path.join(dataDir, 'mcp-reconnect');
|
|
111
|
+
const pluginPath = path.join(pluginDirectory, 'openchamber-mcp-reconnect-plugin.js');
|
|
112
|
+
|
|
113
|
+
const prepareManagedOpenCodeEnv = async (rawConfig) => {
|
|
114
|
+
await fsPromises.mkdir(pluginDirectory, { recursive: true });
|
|
115
|
+
await fsPromises.writeFile(pluginPath, createPluginSource(), { mode: 0o600 });
|
|
116
|
+
return {
|
|
117
|
+
OPENCODE_CONFIG_CONTENT: appendManagedPlugin(rawConfig, pathToFileURL(pluginPath).href, 'MCP reconnect plugin'),
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return { prepareManagedOpenCodeEnv };
|
|
122
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { createMcpReconnectRuntime } from './runtime.js';
|
|
8
|
+
|
|
9
|
+
const temporaryDirectories = [];
|
|
10
|
+
const disposers = [];
|
|
11
|
+
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
await Promise.all(disposers.splice(0).map((dispose) => dispose()));
|
|
14
|
+
vi.useRealTimers();
|
|
15
|
+
await Promise.all(temporaryDirectories.splice(0).map((directory) => fs.rm(directory, { recursive: true, force: true })));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const materialize = async (rawConfig = '{}') => {
|
|
19
|
+
const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'openchamber-mcp-reconnect-'));
|
|
20
|
+
temporaryDirectories.push(dataDir);
|
|
21
|
+
const runtime = createMcpReconnectRuntime({ fsPromises: fs, path, dataDir });
|
|
22
|
+
const prepared = await runtime.prepareManagedOpenCodeEnv(rawConfig);
|
|
23
|
+
const pluginPath = path.join(dataDir, 'mcp-reconnect', 'openchamber-mcp-reconnect-plugin.js');
|
|
24
|
+
const pluginModule = await import(`${pathToFileURL(pluginPath).href}?test=${Date.now()}-${Math.random()}`);
|
|
25
|
+
return { prepared, pluginPath, plugin: pluginModule.AiWorkerMcpReconnectPlugin };
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A stand-in for the SDK client OpenCode hands to plugins. `statuses` is live
|
|
30
|
+
* state the test mutates; `connect` records when each attempt happened.
|
|
31
|
+
*/
|
|
32
|
+
const createClient = (statuses, { onConnect } = {}) => {
|
|
33
|
+
const attempts = [];
|
|
34
|
+
return {
|
|
35
|
+
attempts,
|
|
36
|
+
statuses,
|
|
37
|
+
mcp: {
|
|
38
|
+
status: vi.fn(async () => ({ data: { ...statuses } })),
|
|
39
|
+
connect: vi.fn(async ({ path: { name } }) => {
|
|
40
|
+
attempts.push({ name, at: Date.now() });
|
|
41
|
+
onConnect?.(name);
|
|
42
|
+
return { data: true };
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const start = async (plugin, client) => {
|
|
49
|
+
const hooks = await plugin({ client });
|
|
50
|
+
disposers.push(hooks.dispose);
|
|
51
|
+
return hooks;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
describe('managed MCP reconnect runtime', () => {
|
|
55
|
+
it('materializes the plugin and preserves existing plugin entries', async () => {
|
|
56
|
+
const { prepared, pluginPath } = await materialize('{ "plugin": ["file:///existing.js"], "model": "test/model" }');
|
|
57
|
+
const config = JSON.parse(prepared.OPENCODE_CONFIG_CONTENT);
|
|
58
|
+
expect(config.model).toBe('test/model');
|
|
59
|
+
expect(config.plugin).toEqual(['file:///existing.js', pathToFileURL(pluginPath).href]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('reconnects only servers in the failed state', async () => {
|
|
63
|
+
vi.useFakeTimers();
|
|
64
|
+
const { plugin } = await materialize();
|
|
65
|
+
const client = createClient({
|
|
66
|
+
broken: { status: 'failed', error: 'spawn ENOENT' },
|
|
67
|
+
healthy: { status: 'connected' },
|
|
68
|
+
off: { status: 'disabled' },
|
|
69
|
+
login: { status: 'needs_auth' },
|
|
70
|
+
registration: { status: 'needs_client_registration', error: 'no client id' },
|
|
71
|
+
});
|
|
72
|
+
await start(plugin, client);
|
|
73
|
+
|
|
74
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
75
|
+
|
|
76
|
+
expect(client.attempts.map((attempt) => attempt.name)).toEqual(['broken']);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('backs off per server up to a cap while it keeps failing', async () => {
|
|
80
|
+
vi.useFakeTimers();
|
|
81
|
+
const { plugin } = await materialize();
|
|
82
|
+
const client = createClient({ broken: { status: 'failed', error: 'refused' } });
|
|
83
|
+
const started = Date.now();
|
|
84
|
+
await start(plugin, client);
|
|
85
|
+
|
|
86
|
+
await vi.advanceTimersByTimeAsync(92_000);
|
|
87
|
+
|
|
88
|
+
expect(client.attempts.map((attempt) => attempt.at - started)).toEqual([
|
|
89
|
+
1000, 2000, 4000, 8000, 16_000, 32_000, 62_000, 92_000,
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('stops retrying once a server is back and starts fresh when it drops again', async () => {
|
|
94
|
+
vi.useFakeTimers();
|
|
95
|
+
const { plugin } = await materialize();
|
|
96
|
+
const statuses = { flaky: { status: 'failed', error: 'refused' } };
|
|
97
|
+
const client = createClient(statuses, {
|
|
98
|
+
onConnect: () => { statuses.flaky = { status: 'connected' }; },
|
|
99
|
+
});
|
|
100
|
+
const hooks = await start(plugin, client);
|
|
101
|
+
|
|
102
|
+
await vi.advanceTimersByTimeAsync(60_000);
|
|
103
|
+
expect(client.attempts).toHaveLength(1);
|
|
104
|
+
|
|
105
|
+
statuses.flaky = { status: 'failed', error: 'Connection closed' };
|
|
106
|
+
await hooks.event({ event: { type: 'mcp.tools.changed', properties: { server: 'flaky' } } });
|
|
107
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
108
|
+
|
|
109
|
+
expect(client.attempts).toHaveLength(2);
|
|
110
|
+
expect(client.attempts[1].at - client.attempts[0].at).toBeGreaterThan(30_000);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('keeps going when status is temporarily unavailable', async () => {
|
|
114
|
+
vi.useFakeTimers();
|
|
115
|
+
const { plugin } = await materialize();
|
|
116
|
+
const client = createClient({ broken: { status: 'failed', error: 'refused' } });
|
|
117
|
+
client.mcp.status.mockRejectedValueOnce(new Error('ECONNREFUSED'));
|
|
118
|
+
await start(plugin, client);
|
|
119
|
+
|
|
120
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
121
|
+
expect(client.attempts).toHaveLength(0);
|
|
122
|
+
|
|
123
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
124
|
+
expect(client.attempts).toHaveLength(1);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('does nothing after dispose', async () => {
|
|
128
|
+
vi.useFakeTimers();
|
|
129
|
+
const { plugin } = await materialize();
|
|
130
|
+
const client = createClient({ broken: { status: 'failed', error: 'refused' } });
|
|
131
|
+
const hooks = await plugin({ client });
|
|
132
|
+
|
|
133
|
+
await hooks.dispose();
|
|
134
|
+
await hooks.event({ event: { type: 'mcp.tools.changed', properties: { server: 'broken' } } });
|
|
135
|
+
await vi.advanceTimersByTimeAsync(120_000);
|
|
136
|
+
|
|
137
|
+
expect(client.mcp.status).not.toHaveBeenCalled();
|
|
138
|
+
expect(client.attempts).toHaveLength(0);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Message Queue
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Owns the messages a user queued while a session was busy, and sends them the
|
|
6
|
+
moment the session goes idle. The queue lives in the web server so a closed
|
|
7
|
+
tab, a locked phone, or a dropped connection no longer strands it. Structural
|
|
8
|
+
template: `permission-auto-accept` — the server is authoritative, the shared UI
|
|
9
|
+
renders a projection, and VS Code (which has no server of its own) keeps its
|
|
10
|
+
UI-side queue and foreground auto-send hook.
|
|
11
|
+
|
|
12
|
+
## Files
|
|
13
|
+
|
|
14
|
+
- `runtime.js` — `createMessageQueueRuntime(...)` (state, persistence,
|
|
15
|
+
dispatch loop, event handling) and `registerMessageQueueRoutes(app, runtime)`.
|
|
16
|
+
- `runtime.test.js` — delivery, idleness gates, retries, holds, persistence,
|
|
17
|
+
concurrency with in-flight sends, slash commands, and project knowledge.
|
|
18
|
+
|
|
19
|
+
Wiring: created in `server/index.js` after the global event hub and the
|
|
20
|
+
session-knowledge runtime; routes registered in
|
|
21
|
+
`opencode/feature-routes-runtime.js` (before the generic OpenCode proxy) with
|
|
22
|
+
JSON bodies enabled in `opencode/core-routes.js`; stopped by
|
|
23
|
+
`opencode/shutdown-runtime.js`.
|
|
24
|
+
|
|
25
|
+
## Item
|
|
26
|
+
|
|
27
|
+
An item is what the UI would have sent itself, captured at queue time so the
|
|
28
|
+
send never re-resolves mutable UI state:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
{
|
|
32
|
+
id, createdAt,
|
|
33
|
+
content, // raw text for display and editing
|
|
34
|
+
text, // text to deliver (agent mention stripped, file mentions resolved); defaults to content
|
|
35
|
+
agentMention?, // delivered as an `agent` part
|
|
36
|
+
attachments: [{ id, filename, mimeType, size, source, serverPath?, dataUrl }],
|
|
37
|
+
context: [ // what the composer had attached, in send order
|
|
38
|
+
{ kind: 'context', text, metadata, instructions? }, // a draft chip or linked issue/PR; metadata is the UI's structured payload
|
|
39
|
+
{ kind: 'instruction', text }, // derived from the text (skill instruction)
|
|
40
|
+
{ kind: 'synthetic', text }, // handed to the composer by another surface
|
|
41
|
+
],
|
|
42
|
+
sendConfig: { providerID, modelID, agent?, variant? } // required
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The server is a courier for `context`: it validates the shape (a kind it
|
|
47
|
+
knows, a `metadata` object on `context` entries) and delivers each entry as a
|
|
48
|
+
synthetic text part, an entry's `instructions` going out as its own part just
|
|
49
|
+
before it and its `metadata` riding the part verbatim so the timeline renders
|
|
50
|
+
the context block back. The payload inside `metadata` is the UI's contract
|
|
51
|
+
(`lib/messages/contextParts.ts`), parsed by the UI on the way back.
|
|
52
|
+
|
|
53
|
+
`parseQueuedItemInput` rejects anything the server could not deliver later
|
|
54
|
+
(no text, attachments, or context; missing model; malformed attachment or
|
|
55
|
+
context entry). Public snapshots and broadcasts strip the payloads —
|
|
56
|
+
attachment `dataUrl` (megabytes of base64) and `context` (a PR diff, say) —
|
|
57
|
+
so they do not ride every update; the only way to get them back is a `take`.
|
|
58
|
+
|
|
59
|
+
## Persistence
|
|
60
|
+
|
|
61
|
+
`<data-dir>/message-queue.json` (`AIWORKER_DATA_DIR` or
|
|
62
|
+
`~/.config/openchamber`): `{ version, revision, sessions: { [sessionId]:
|
|
63
|
+
{ directory, items } } }`, written atomically (temp file + rename) through a
|
|
64
|
+
serialized write chain. A missing file is an empty queue. A malformed file is
|
|
65
|
+
a failure, not an empty queue: it is moved aside as
|
|
66
|
+
`message-queue.json.corrupt-<timestamp>` before the runtime starts empty, so
|
|
67
|
+
the next write cannot overwrite the user's data. A failed read leaves writes
|
|
68
|
+
disabled until a later load succeeds. `revision` is a global monotonic counter
|
|
69
|
+
bumped on every mutation; clients use it to reject stale snapshots.
|
|
70
|
+
|
|
71
|
+
In-memory only, deliberately: the in-flight item (`sendingId`), retry
|
|
72
|
+
backoff, abort timestamps, and holds. A restart has no in-flight sends; a
|
|
73
|
+
persisted "sending" flag would strand a message forever.
|
|
74
|
+
|
|
75
|
+
## Delivery loop
|
|
76
|
+
|
|
77
|
+
1. `start()` subscribes to the global upstream hub and loads the file; on
|
|
78
|
+
load and on every hub `connect` it arms every session that has items.
|
|
79
|
+
2. `session.status` for a queued session: `idle` arms a short quiet timer
|
|
80
|
+
(500 ms, coalescing the burst around a turn boundary), `busy`/`retry`
|
|
81
|
+
clears it. A `message.updated` for a completed assistant reply arms as
|
|
82
|
+
well, so a missed idle event cannot strand the queue. `session.deleted`
|
|
83
|
+
drops the session's queue. An assistant `MessageAbortedError` records an
|
|
84
|
+
abort.
|
|
85
|
+
3. `tick(sessionId)` bails when the queue is empty, an item is in flight, or
|
|
86
|
+
the session is held. It re-arms after a 2 s post-abort hold (the UI's
|
|
87
|
+
old behavior: a stop is not immediately followed by the next prompt) or
|
|
88
|
+
while the head item is in retry backoff.
|
|
89
|
+
4. Idleness is re-verified against OpenCode before sending, because
|
|
90
|
+
`prompt_async` into a running turn steers into it instead of starting the
|
|
91
|
+
next one: `GET /session/status` must not list the session as busy/retry,
|
|
92
|
+
and the trailing message must not be an unfinished assistant reply (the
|
|
93
|
+
status map only lists busy sessions, so a missed busy event leaves no
|
|
94
|
+
entry while a turn still streams). A failed fetch is unknown, never idle:
|
|
95
|
+
the tick re-arms with backoff.
|
|
96
|
+
5. The head is marked in flight (broadcast), then sent:
|
|
97
|
+
- text starting with `/` that names a command in OpenCode's `/command`
|
|
98
|
+
list (skills included) and carries no captured context goes to
|
|
99
|
+
`POST /session/:id/command` with the captured model, agent, variant, and
|
|
100
|
+
file parts. That route accepts file parts only, so a command queued
|
|
101
|
+
**with** context takes the prompt route instead, the same rule the
|
|
102
|
+
composer applies: the command's template is expanded with its arguments
|
|
103
|
+
(`$ARGUMENTS`, `$1..$N`, or appended), a skill keeps its `/name args` text
|
|
104
|
+
and gets an explicit "the user invoked this skill" synthetic part after
|
|
105
|
+
the context;
|
|
106
|
+
- otherwise `POST /session/:id/prompt_async` with the parts in the same
|
|
107
|
+
order a UI send uses: text, files, the captured context, the skill
|
|
108
|
+
invocation when there is one, pending project knowledge
|
|
109
|
+
(`sessionKnowledgeRuntime.resolvePendingForSession`, synthetic, recorded
|
|
110
|
+
as delivered only after the prompt is accepted), then the agent mention.
|
|
111
|
+
Success removes the item, persists, broadcasts, and marks the user
|
|
112
|
+
message sent for notifications. Failure keeps the item, backs off
|
|
113
|
+
2 s → 60 s (doubling per consecutive failure of that item), and re-arms.
|
|
114
|
+
6. The next item goes out after the next busy → idle cycle.
|
|
115
|
+
|
|
116
|
+
## Holds
|
|
117
|
+
|
|
118
|
+
Auto-review is driven from the UI and bounces the original session through
|
|
119
|
+
idle between iterations; the UI tells the server to hold that session's queue
|
|
120
|
+
(`PUT .../hold { held: true, ttlMs? }`) while a run is going and releases it
|
|
121
|
+
when the run ends. A hold expires on its own (default 5 min, cap 10 min)
|
|
122
|
+
because the UI that asserted it may be gone; the UI re-asserts it every two
|
|
123
|
+
minutes while the run continues. Releasing arms a dispatch.
|
|
124
|
+
|
|
125
|
+
## Routes (`/api/message-queue`)
|
|
126
|
+
|
|
127
|
+
Normal authenticated AiWorker runtime routes; never on browser URL-token
|
|
128
|
+
allowlists.
|
|
129
|
+
|
|
130
|
+
| Route | Purpose |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `GET /api/message-queue` | Full snapshot `{ revision, sessions[] }` |
|
|
133
|
+
| `POST .../sessions/:id/items` | Append `{ directory, item }`; returns `{ revision, session, itemId }` and arms a dispatch (the session may already be idle) |
|
|
134
|
+
| `DELETE .../sessions/:id/items/:itemId` | Remove; `409` while that item is in flight |
|
|
135
|
+
| `POST .../sessions/:id/items/:itemId/take` | Remove and return the full item (payloads included); `404`/`409` |
|
|
136
|
+
| `POST .../sessions/:id/take` | Remove and return every item not in flight, in order |
|
|
137
|
+
| `PUT .../sessions/:id/order` | `{ itemIds }` must be a complete permutation |
|
|
138
|
+
| `DELETE .../sessions/:id` | Clear; the in-flight item stays |
|
|
139
|
+
| `PUT .../sessions/:id/hold` | `{ held, ttlMs? }` |
|
|
140
|
+
|
|
141
|
+
Every mutation broadcasts `aiworker:message-queue.updated` with
|
|
142
|
+
`{ revision, session }` to all connected clients (SSE and WS), so several
|
|
143
|
+
devices on one server see one queue. SSE uses the shared control stream at
|
|
144
|
+
`/api/openchamber/events`; `/api/global/event` carries no AiWorker events.
|
|
145
|
+
The UI subscribes independently of its OpenCode transport and re-reads the
|
|
146
|
+
snapshot whenever either stream reconnects. The session in that payload always names
|
|
147
|
+
its `directory`, including the broadcast that removes the last item: the UI
|
|
148
|
+
keys its projection by directory, and a broadcast without one left the
|
|
149
|
+
delivered message on screen (a session's directory is remembered until the
|
|
150
|
+
session is deleted or evicted).
|
|
151
|
+
|
|
152
|
+
Limits: 20 items per session, 50 sessions (oldest evicted, never one with an
|
|
153
|
+
item in flight), 200k characters of content; attachment payloads are bounded
|
|
154
|
+
by the route family's 50 MB JSON limit.
|
|
155
|
+
|
|
156
|
+
## UI ownership
|
|
157
|
+
|
|
158
|
+
`packages/ui/src/stores/messageQueueStore.ts` is the projection: see its
|
|
159
|
+
section in `packages/ui/src/stores/DOCUMENTATION.md`. VS Code intentionally
|
|
160
|
+
does not use this module; with all AiWorker webviews closed, queued
|
|
161
|
+
messages there are not delivered.
|