@vantran-se/openchamber-web 2.1.1 → 2.2.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 +32 -24
- package/bin/cli.js +1 -3
- package/bin/cli.test.js +40 -16
- package/bin/lib/DOCUMENTATION.md +6 -5
- package/bin/lib/cli-args.js +6 -1
- package/bin/lib/cli-args.test.js +34 -0
- package/bin/lib/cli-lifecycle.js +0 -1
- package/bin/lib/cli-network.js +8 -2
- package/bin/lib/cli-process.js +1 -1
- package/bin/lib/cli-settings-accessors.test.js +21 -13
- package/bin/lib/cli-startup.js +17 -11
- package/bin/lib/cli-startup.test.js +92 -2
- package/bin/lib/commands-connect-url.js +8 -1
- package/bin/lib/commands-schedule.js +8 -2
- package/bin/lib/commands-serve.js +15 -21
- package/bin/lib/commands-update.js +12 -24
- package/bin/lib/commands-update.test.js +14 -46
- package/dist/assets/DiffView-Cv9MSXl1.js +4 -0
- package/dist/assets/FilePreviewCommentMenu-D4kLseHj.js +4 -0
- package/dist/assets/FilesView-BVoD8I2x.js +14 -0
- package/dist/assets/{GitView-DzuzhIPx.js → GitView-Bzb8p-ew.js} +4 -4
- package/dist/assets/GuestSurfacePane-Crdbvsxo.js +1 -0
- package/dist/assets/InProgressOperationBanner-CwMjNQgk.js +2 -0
- package/dist/assets/{JsonTreeView-B_EPRjzy.js → JsonTreeView-DRKHZfsn.js} +1 -1
- package/dist/assets/MarkdownImageGallery-DZtqT1Zn.js +1 -0
- package/dist/assets/MarkdownRendererImpl-CqG7HCNd.js +10 -0
- package/dist/assets/OnboardingScreen-DMIxwYK5.js +1 -0
- package/dist/assets/{PatchDiff-6yh1csza.js → PatchDiff-CUcCg1jk.js} +1 -1
- package/dist/assets/PierreDiffViewer-CnnR-dnD.js +228 -0
- package/dist/assets/PlanView-RCBadcY7.js +5 -0
- package/dist/assets/SettingsView-CjBjZFRO.js +30 -0
- package/dist/assets/SettingsWindow-DyM7QuhI.js +1 -0
- package/dist/assets/SubmoduleDiffSummary-BWIfgMED.js +1 -0
- package/dist/assets/ToolOutputDialog-Q0P3FuAi.js +15 -0
- package/dist/assets/ToolPartDiffPreview-p7qR9FpU.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-BTJVRkyo.js → VirtualizedFileDiff-CFpjL8Ns.js} +3 -3
- package/dist/assets/{Virtualizer-xCEYz34p.js → Virtualizer-C0IsclZn.js} +1 -1
- package/dist/assets/WalkthroughView-DI31zD02.js +4 -0
- package/dist/assets/appThemeRegistry-B9gkFq9i.js +1 -0
- package/dist/assets/{computeVirtualFileMetrics-CmPe29DM.js → computeVirtualFileMetrics-C2WM6JC2.js} +2 -2
- package/dist/assets/de-YKfk5MmC.js +14 -0
- package/dist/assets/es-_gwT0kr-.js +20 -0
- package/dist/assets/fileTreeStatus-D438yi8M.js +1 -0
- package/dist/assets/fr-DqzvXmIm.js +18 -0
- package/dist/assets/index-BLaHKdtx.js +1 -0
- package/dist/assets/index-BkMXRrVQ.css +1 -0
- package/dist/assets/index-DxNa7xYB.js +65 -0
- package/dist/assets/initializeSharedPreferences-Bck5EAWP.js +1 -0
- package/dist/assets/{isDiffPlainText-CQmDCnEv.js → isDiffPlainText-Dgbw8VKk.js} +1 -1
- package/dist/assets/{isFilePlainText-CYBUzW4I.js → isFilePlainText-_ZS5UYRj.js} +1 -1
- package/dist/assets/ja-B-GFYhQE.js +20 -0
- package/dist/assets/ko-mzksfqDC.js +20 -0
- package/dist/assets/main-C5Xx8dn_.js +2 -0
- package/dist/assets/main-CfBSQM1H.js +990 -0
- package/dist/assets/markdownCore-BmWwW1ll.js +7 -0
- package/dist/assets/miniChat-mnKwy4vQ.js +2 -0
- package/dist/assets/mobile-DeEQd0wZ.js +2 -0
- package/dist/assets/nl-CWQr6NSq.js +20 -0
- package/dist/assets/{parseDiffFromFile-Cy0PNxun.js → parseDiffFromFile-BUgn-T9p.js} +2 -2
- package/dist/assets/patchFileDiff-Dt8LhgBh.js +13 -0
- package/dist/assets/pl-UmRcDUX2.js +20 -0
- package/dist/assets/pt-BR-BSLybvVF.js +20 -0
- package/dist/assets/relocateRestoredSession-B5E9xb3T.js +1 -0
- package/dist/assets/renderElectronMiniChatApp-C0a9A5_J.js +2 -0
- package/dist/assets/renderMobileApp-BNncg3Pk.js +25 -0
- package/dist/assets/runtimeEndpointReset-CZ5aafpl.js +62 -0
- package/dist/assets/{selectionMarkdown-TkYk5h9l.js → selectionMarkdown-CUN3wUld.js} +9 -9
- package/dist/assets/{sessionOwnership-Bs1xbRfB.js → sessionOwnership-BSuxCEhb.js} +1 -1
- package/dist/assets/shikiHighlight-DHDx-6eM.js +1 -0
- package/dist/assets/space-apply-DM_2pRW7.js +1 -0
- package/dist/assets/space-close-Bpomt8z0.js +1 -0
- package/dist/assets/{surface-B6n7wJtt.js → surface-C-jQaGnL.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-CDlKffWz.js → textMateThemeFromAppTheme-DlsTphbY.js} +1 -1
- package/dist/assets/tr-gaTrX_HK.js +11 -0
- package/dist/assets/uk-CnJF6ykl.js +20 -0
- package/dist/assets/{useAppFontEffects-DCqCVGlT.js → useAppFontEffects-DGlvOqP_.js} +194 -233
- package/dist/assets/useInlineCommentController-kMN4Wg_D.js +2 -0
- package/dist/assets/{useRootScrollLock-BowwS8kq.js → useRootScrollLock-DMPpvpNP.js} +2 -2
- package/dist/assets/{vendor-base-ui-DX96QhTP.js → vendor-base-ui-Det7U81h.js} +1 -1
- package/dist/assets/{vendor-codemirror-autocomplete-DW1FQz9_.js → vendor-codemirror-autocomplete-BSHQHbKu.js} +1 -1
- package/dist/assets/{vendor-codemirror-commands-DpmhalwN.js → vendor-codemirror-commands-C4G_dPWe.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-angular-BqExbN1M.js → vendor-codemirror-lang-angular-B6LbBGHB.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-cpp-DZA36HfS.js → vendor-codemirror-lang-cpp-BWWXBx7W.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-css-B07blnnZ.js → vendor-codemirror-lang-css-Cayfb29q.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-go-BM8K9D1-.js → vendor-codemirror-lang-go-BoKlCNKw.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-html-DaH9adfE.js → vendor-codemirror-lang-html-BQWerHsJ.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-java-CsdC0viz.js → vendor-codemirror-lang-java-DWX7Fq_K.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-javascript-CpGYXGuo.js → vendor-codemirror-lang-javascript-DbpEcZM0.js} +2 -2
- package/dist/assets/{vendor-codemirror-lang-jinja-BBeGzXw5.js → vendor-codemirror-lang-jinja-D1JIJRA7.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-json-58T70Hqs.js → vendor-codemirror-lang-json-BzgckzmB.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-less-BI-xCA59.js → vendor-codemirror-lang-less-B9EpDXFi.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-liquid-zEcMa-zy.js → vendor-codemirror-lang-liquid-9U7kRbSf.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-markdown-BLKnlLP4.js → vendor-codemirror-lang-markdown-D2TEBIvZ.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-php-9f35M83-.js → vendor-codemirror-lang-php-BT6sDVzE.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-python-jAMtYiA9.js +3 -0
- package/dist/assets/{vendor-codemirror-lang-rust-MNDL_jZQ.js → vendor-codemirror-lang-rust-CVO56EiL.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-sass-p9epsSNm.js → vendor-codemirror-lang-sass-DZlOoUWu.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-sql-Bqzh0EXX.js → vendor-codemirror-lang-sql-fZ6t1DPw.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-vue-BnNUWV-p.js → vendor-codemirror-lang-vue-DxbqrLiE.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-wast-C2a-oLmk.js → vendor-codemirror-lang-wast-DyU1Gt-I.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-xml-C9a_K5_p.js +1 -0
- package/dist/assets/{vendor-codemirror-lang-yaml-C0sjswB0.js → vendor-codemirror-lang-yaml-Cptq8l0e.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-ByBn5zIW.js → vendor-codemirror-language-DufXoQvE.js} +2 -2
- package/dist/assets/vendor-codemirror-language-data-CtwSWKtX.js +2 -0
- package/dist/assets/{vendor-codemirror-search-ib-sHD5W.js → vendor-codemirror-search-CvXM_pZg.js} +1 -1
- package/dist/assets/{vendor-codemirror-state-G3UuR-be.js → vendor-codemirror-state-Dek9sMtw.js} +1 -1
- package/dist/assets/{vendor-codemirror-view-Bniqp7p5.js → vendor-codemirror-view-1e0FQ6Wn.js} +1 -1
- package/dist/assets/vendor-framer-motion-DxDVhOLc.js +9 -0
- package/dist/assets/vendor-katex-CuQvj9-L.js +257 -0
- package/dist/assets/vendor-katex-DdZNUVNG.css +1 -0
- package/dist/assets/{vendor-motion-dom-DcCgeG69.js → vendor-motion-dom-HXPOMxb7.js} +1 -1
- package/dist/assets/{vendor-opencode-client-DHzhmLwV.js → vendor-opencode-client-BmFcUkbQ.js} +1 -1
- package/dist/assets/{vendor-replit-codemirror-vim-DkzqxN2H.js → vendor-replit-codemirror-vim-BlJRn-cW.js} +2 -2
- package/dist/assets/{vendor-tanstack-react-virtual-H_JmXOFV.js → vendor-tanstack-react-virtual-CvuLsk7D.js} +1 -1
- package/dist/assets/{vendor-tanstack-virtual-core-Dxc8hSTB.js → vendor-tanstack-virtual-core-DPaNcJcT.js} +1 -1
- package/dist/assets/zh-CN-t2vigSB1.js +20 -0
- package/dist/assets/zh-TW-jAgqBokB.js +20 -0
- package/dist/index.html +6 -6
- package/dist/mini-chat.html +6 -6
- package/dist/mobile.html +6 -6
- package/package.json +8 -5
- package/server/index.js +80 -36
- package/server/lib/agent-memory/actions.js +1 -6
- package/server/lib/agent-memory/routes.js +1 -2
- package/server/lib/agent-memory/runtime.js +1 -8
- package/server/lib/agent-tool/DOCUMENTATION.md +7 -3
- package/server/lib/agent-tool/runtime.js +3 -6
- package/server/lib/agent-tool/runtime.test.js +0 -2
- package/server/lib/browser-control/DOCUMENTATION.md +6 -0
- package/server/lib/changelog/update-notes.js +1 -1
- package/server/lib/context-obligatory/runtime.js +1 -2
- package/server/lib/dev-tunnel/DOCUMENTATION.md +8 -0
- package/server/lib/dev-tunnel/client.js +21 -10
- package/server/lib/dev-tunnel/tunnel.test.js +34 -0
- package/server/lib/dispatch-results/runtime.js +1 -1
- package/server/lib/enterprise-mode.d.ts +1 -0
- package/server/lib/enterprise-mode.js +13 -2
- package/server/lib/enterprise-mode.test.js +21 -0
- package/server/lib/event-stream/DOCUMENTATION.md +2 -2
- package/server/lib/event-stream/index.js +1 -3
- package/server/lib/event-stream/translate-v2.js +1 -9
- package/server/lib/event-stream/translate-v2.test.js +0 -11
- package/server/lib/fs/DOCUMENTATION.md +5 -2
- package/server/lib/fs/routes.js +62 -15
- package/server/lib/fs/routes.test.js +278 -48
- package/server/lib/fs/workspace-file-names.js +104 -0
- package/server/lib/fs/workspace-file-names.test.js +62 -0
- package/server/lib/git/DOCUMENTATION.md +17 -5
- package/server/lib/git/contributor-provenance-storage.js +9 -6
- package/server/lib/git/contributor-provenance-storage.test.js +29 -0
- package/server/lib/git/credential-broker.js +1 -1
- package/server/lib/git/credential-resolver.js +1 -4
- package/server/lib/git/credentials.js +1 -0
- package/server/lib/git/discovery-endpoint.js +1 -1
- package/server/lib/git/identity-provisioning.js +2 -1
- package/server/lib/git/identity-storage.js +3 -4
- package/server/lib/git/lfs-discovery.js +14 -14
- package/server/lib/git/lfs-discovery.test.js +16 -6
- package/server/lib/git/network-operation-plan.js +50 -62
- package/server/lib/git/network-operation-plan.test.js +103 -1
- package/server/lib/git/network-operation-registry.js +4 -6
- package/server/lib/git/network-operation-storage.js +4 -6
- package/server/lib/git/network-operations.js +12 -4
- package/server/lib/git/network-operations.test.js +19 -6
- package/server/lib/git/redaction.js +2 -1
- package/server/lib/git/repository-credential-runtime.js +5 -3
- package/server/lib/git/repository-credential-runtime.test.js +22 -0
- package/server/lib/git/routes.js +31 -137
- package/server/lib/git/routes.test.js +2 -2
- package/server/lib/git/service.js +143 -27
- package/server/lib/git/service.test.js +215 -1
- package/server/lib/git/ssh-credential-storage.js +1 -4
- package/server/lib/git/submodule-discovery.js +1 -1
- package/server/lib/git/worktree-bootstrap-storage.js +3 -4
- package/server/lib/git/worktree-bootstrap-storage.test.js +15 -0
- package/server/lib/git/worktree-creation.js +194 -0
- package/server/lib/git/worktree-creation.test.js +155 -0
- package/server/lib/github/DOCUMENTATION.md +11 -6
- package/server/lib/github/auth.js +2 -4
- package/server/lib/github/device-flow.js +2 -2
- package/server/lib/github/gh-cli-credential.js +1 -0
- package/server/lib/github/octokit.js +31 -0
- package/server/lib/github/pr-status.js +35 -18
- package/server/lib/github/pr-status.test.js +271 -3
- package/server/lib/github/reference-search.js +102 -13
- package/server/lib/github/reference-search.test.js +48 -7
- package/server/lib/github/routes.auth.test.js +18 -0
- package/server/lib/github/routes.js +360 -6
- package/server/lib/github/routes.mutations.test.js +239 -0
- package/server/lib/github/routes.test.js +134 -2
- package/server/lib/gitlab/DOCUMENTATION.md +5 -1
- package/server/lib/gitlab/auth-storage.test.js +1 -0
- package/server/lib/gitlab/mappers.js +30 -1
- package/server/lib/gitlab/resources.js +230 -6
- package/server/lib/gitlab/resources.test.js +179 -0
- package/server/lib/gitlab/routes.js +200 -6
- package/server/lib/gitlab/routes.test.js +186 -2
- package/server/lib/gitlab/validation.js +1 -5
- package/server/lib/guests/DOCUMENTATION.md +1 -1
- package/server/lib/guests/oauth.js +2 -2
- package/server/lib/guests/service.js +1 -1
- package/server/lib/guests/sockets.js +1 -1
- package/server/lib/injected-env.d.ts +8 -0
- package/server/lib/injected-env.js +50 -0
- package/server/lib/injected-env.test.js +56 -0
- package/server/lib/linear/DOCUMENTATION.md +5 -6
- package/server/lib/linear/issues.js +62 -4
- package/server/lib/linear/issues.test.js +68 -0
- package/server/lib/linear/oauth.js +5 -5
- package/server/lib/linear/parse.js +2 -9
- package/server/lib/message-queue/DOCUMENTATION.md +23 -1
- package/server/lib/message-queue/runtime.js +85 -12
- package/server/lib/message-queue/runtime.test.js +172 -2
- package/server/lib/notifications/apns-runtime.js +5 -6
- package/server/lib/notifications/apns-runtime.test.js +20 -4
- package/server/lib/openchamber-control/DOCUMENTATION.md +19 -1
- package/server/lib/openchamber-control/actions.js +2 -1
- package/server/lib/openchamber-control/service.js +119 -20
- package/server/lib/openchamber-control/service.test.js +166 -0
- package/server/lib/openchamber-sessions/archive-store.js +1 -6
- package/server/lib/openchamber-sessions/routes.js +47 -12
- package/server/lib/openchamber-sessions/routes.test.js +65 -2
- package/server/lib/openchamber-sessions/session-metadata-store.js +1 -6
- package/server/lib/opencode/DOCUMENTATION.md +22 -8
- package/server/lib/opencode/agents.js +62 -0
- package/server/lib/opencode/auth-state-runtime.js +3 -1
- package/server/lib/opencode/auth.d.ts +1 -1
- package/server/lib/opencode/auth.js +8 -0
- package/server/lib/opencode/auth.test.js +31 -0
- package/server/lib/opencode/config-entity-routes.js +16 -0
- package/server/lib/opencode/config-entity-v2.test.js +16 -1
- package/server/lib/opencode/config-paths.test.js +2 -2
- package/server/lib/opencode/config-v2.js +2 -4
- package/server/lib/opencode/feature-routes-runtime.js +89 -49
- package/server/lib/opencode/lifecycle.js +41 -21
- package/server/lib/opencode/lifecycle.test.js +50 -2
- package/server/lib/opencode/model-discovery.js +1 -1
- package/server/lib/opencode/model-discovery.test.js +17 -0
- package/server/lib/opencode/models-metadata.js +0 -2
- package/server/lib/opencode/openchamber-routes.test.js +3 -3
- package/server/lib/opencode/plugins.js +6 -9
- package/server/lib/opencode/provider-source-route.test.js +101 -3
- package/server/lib/opencode/providers-disabled.test.js +59 -0
- package/server/lib/opencode/providers.js +34 -0
- package/server/lib/opencode/proxy.js +11 -3
- package/server/lib/opencode/routes.js +40 -2
- package/server/lib/opencode/settings-helpers.js +27 -1
- package/server/lib/opencode/settings-helpers.test.js +19 -7
- package/server/lib/opencode/settings-registry.json +27 -16
- package/server/lib/opencode/settings-runtime.test.js +18 -0
- package/server/lib/opencode/shared-service-runtime.js +23 -104
- package/server/lib/opencode/shared.js +77 -46
- package/server/lib/opencode/shared.test.js +12 -12
- package/server/lib/opencode/shutdown-runtime.test.js +2 -3
- package/server/lib/opencode/skill-routes.js +123 -38
- package/server/lib/opencode/skill-routes.test.js +201 -0
- package/server/lib/opencode/skills.js +81 -34
- package/server/lib/opencode/skills.test.js +176 -21
- package/server/lib/opencode/startup-pipeline-runtime.js +6 -3
- package/server/lib/opencode/startup-pipeline-runtime.test.js +2 -1
- package/server/lib/opencode/watcher.test.js +0 -54
- package/server/lib/outbound-proxy.js +52 -0
- package/server/lib/outbound-proxy.test.js +55 -0
- package/server/lib/package-manager.js +23 -17
- package/server/lib/package-manager.test.js +5 -18
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +1 -1
- package/server/lib/permission-auto-accept/runtime.js +19 -47
- package/server/lib/permission-auto-accept/runtime.test.js +2 -2
- package/server/lib/project-context/DOCUMENTATION.md +7 -1
- package/server/lib/project-context/routes.js +1 -2
- package/server/lib/project-context/runtime.js +12 -14
- package/server/lib/project-context/runtime.test.js +78 -2
- package/server/lib/projects/project-config.js +30 -15
- package/server/lib/projects/project-config.test.js +30 -0
- package/server/lib/projects/project-setup.js +1 -2
- package/server/lib/projects/routes.js +1 -2
- package/server/lib/quota/DOCUMENTATION.md +35 -7
- package/server/lib/quota/index.js +1 -0
- package/server/lib/quota/providers/claude/index.js +1 -1
- package/server/lib/quota/providers/cline-pass.js +1 -1
- package/server/lib/quota/providers/exe-dev.js +1 -1
- package/server/lib/quota/providers/google/index.js +1 -1
- package/server/lib/quota/providers/hyper.js +1 -1
- package/server/lib/quota/providers/kimi.js +116 -36
- package/server/lib/quota/providers/kimi.test.js +128 -3
- package/server/lib/quota/providers/opencode-go.js +144 -21
- package/server/lib/quota/providers/opencode-go.test.js +229 -6
- package/server/lib/quota/providers/openrouter.js +1 -1
- package/server/lib/quota/providers/zenmux.js +1 -1
- package/server/lib/quota/utils/auth.js +1 -0
- package/server/lib/quota/utils/transformers.js +2 -6
- package/server/lib/relay/DOCUMENTATION.md +18 -3
- package/server/lib/relay/e2ee.js +7 -7
- package/server/lib/relay/host-client.js +84 -13
- package/server/lib/relay/host-client.test.js +201 -1
- package/server/lib/relay/identity.js +6 -29
- package/server/lib/relay/identity.test.js +18 -28
- package/server/lib/relay/key-store.js +141 -0
- package/server/lib/relay/key-store.test.js +138 -0
- package/server/lib/relay/service.js +8 -8
- package/server/lib/relay/service.test.js +10 -5
- package/server/lib/relay/signing-key.js +13 -36
- package/server/lib/relay/tunnel-codec.js +10 -10
- package/server/lib/route-module-exports.test.js +22 -0
- package/server/lib/routing/DOCUMENTATION.md +23 -3
- package/server/lib/routing/cache.js +87 -0
- package/server/lib/routing/defaults.js +11 -0
- package/server/lib/routing/history.js +12 -4
- package/server/lib/routing/runtime.js +39 -3
- package/server/lib/routing/runtime.test.js +81 -2
- package/server/lib/routing/store.js +12 -1
- package/server/lib/routing/store.test.js +13 -0
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +59 -1
- package/server/lib/scheduled-tasks/existing-session.js +119 -0
- package/server/lib/scheduled-tasks/existing-session.test.js +350 -0
- package/server/lib/scheduled-tasks/loops.js +1 -8
- package/server/lib/scheduled-tasks/routes.js +8 -8
- package/server/lib/scheduled-tasks/runtime.js +103 -13
- package/server/lib/scheduled-tasks/runtime.test.js +48 -0
- package/server/lib/scheduled-tasks/service.js +33 -11
- package/server/lib/scheduled-tasks/service.test.js +128 -2
- package/server/lib/scheduled-tasks/session-defaults.js +27 -0
- package/server/lib/scheduled-tasks/session-defaults.test.js +19 -0
- package/server/lib/session-assist/context.js +1 -0
- package/server/lib/session-folders/routes.js +2 -2
- package/server/lib/session-goal/objectives.js +1 -1
- package/server/lib/session-knowledge/routes.js +2 -2
- package/server/lib/session-knowledge/runtime.js +1 -2
- package/server/lib/session-work/DOCUMENTATION.md +39 -14
- package/server/lib/session-work/questions.js +31 -2
- package/server/lib/session-work/runtime.js +22 -13
- package/server/lib/session-work/runtime.test.js +43 -26
- package/server/lib/session-work/state.js +10 -4
- package/server/lib/session-work/state.test.js +8 -1
- package/server/lib/shared/DOCUMENTATION.md +89 -0
- package/server/lib/shared/guards.js +53 -0
- package/server/lib/shared/guards.test.js +112 -0
- package/server/lib/skills-catalog/DOCUMENTATION.md +10 -8
- package/server/lib/skills-catalog/cache.js +3 -2
- package/server/lib/skills-catalog/curated-sources.js +5 -1
- package/server/lib/skills-catalog/git.js +107 -20
- package/server/lib/skills-catalog/git.test.js +78 -0
- package/server/lib/skills-catalog/install.js +21 -12
- package/server/lib/skills-catalog/scan.js +25 -15
- package/server/lib/skills-catalog/source.js +26 -1
- package/server/lib/skills-catalog/source.test.js +27 -0
- package/server/lib/source-control/DOCUMENTATION.md +2 -1
- package/server/lib/source-control/audit-storage.js +7 -6
- package/server/lib/source-control/audit-storage.test.js +11 -0
- package/server/lib/source-control/binding-service.js +2 -5
- package/server/lib/source-control/change-request-worktree.js +90 -0
- package/server/lib/source-control/change-request-worktree.test.js +121 -0
- package/server/lib/source-control/mutation-executor.js +1 -1
- package/server/lib/source-control/mutation-storage.js +11 -5
- package/server/lib/source-control/provider-instance.js +1 -2
- package/server/lib/source-control/repository-identity.js +1 -1
- package/server/lib/source-control/routes.js +14 -2
- package/server/lib/spaces/DOCUMENTATION.md +42 -17
- package/server/lib/spaces/command-stream.test.js +15 -0
- package/server/lib/spaces/dispatcher.js +7 -0
- package/server/lib/spaces/dispatcher.test.js +17 -0
- package/server/lib/spaces/hardening.js +2 -2
- package/server/lib/spaces/hardening.test.js +1 -1
- package/server/lib/spaces/host.js +2 -0
- package/server/lib/spaces/journey.js +66 -5
- package/server/lib/spaces/journey.test.js +95 -4
- package/server/lib/spaces/layout.js +1 -1
- package/server/lib/spaces/places/docker-disk.js +38 -14
- package/server/lib/spaces/places/docker-disk.test.js +80 -1
- package/server/lib/spaces/places/docker.js +35 -11
- package/server/lib/spaces/places/fake-docker.js +22 -3
- package/server/lib/spaces/places/memory-place.js +3 -0
- package/server/lib/spaces/places/packed.docker.live.test.js +1 -1
- package/server/lib/spaces/routes.js +5 -0
- package/server/lib/spaces/routes.test.js +4 -2
- package/server/lib/spaces/run-command.js +9 -2
- package/server/lib/spaces/tools.js +4 -5
- package/server/lib/spaces/tools.test.js +2 -2
- package/server/lib/terminal/runtime.js +18 -6
- package/server/lib/terminal/runtime.test.js +66 -0
- package/server/lib/tts/index.js +3 -0
- package/server/lib/tts/service.js +4 -2
- package/server/lib/walkthrough/DOCUMENTATION.md +3 -1
- package/server/lib/walkthrough/index.js +2 -3
- package/server/lib/walkthrough/pr-source.test.js +22 -0
- package/server/lib/walkthrough/sources.js +6 -1
- package/server/opencode-proxy.test.js +127 -0
- package/bin/lib/commands-startup.test.js +0 -15
- package/dist/assets/CodeMirrorCommentWidgets-CVDSjcrP.js +0 -1
- package/dist/assets/DiffView-Ct7vL3fD.js +0 -4
- package/dist/assets/FilesView-DT0uEYgT.js +0 -17
- package/dist/assets/GuestSurfacePane-DVvk1VPu.js +0 -1
- package/dist/assets/LinearIssuesView-C10MwA9h.js +0 -1
- package/dist/assets/MarkdownImageGallery-BbN_K_3d.js +0 -1
- package/dist/assets/MarkdownRendererImpl-BLt2Tkk9.js +0 -10
- package/dist/assets/OnboardingScreen-Bgp5A3WB.js +0 -1
- package/dist/assets/PierreDiffViewer-CWJLj-S-.js +0 -210
- package/dist/assets/PlanView-DsRWnsOI.js +0 -5
- package/dist/assets/PublishDialog-CCeKloRu.js +0 -2
- package/dist/assets/SettingsView-BuPPsNPp.js +0 -30
- package/dist/assets/SettingsWindow-CM5fL8Ot.js +0 -1
- package/dist/assets/SubmoduleDiffSummary-CfsMcfiQ.js +0 -1
- package/dist/assets/ToolOutputDialog-mZrfSofz.js +0 -15
- package/dist/assets/ToolPartDiffPreview-BUGCRYEL.js +0 -8
- package/dist/assets/WalkthroughView-ClBo7PPd.js +0 -4
- package/dist/assets/appThemeRegistry-CxzMxR-H.js +0 -1
- package/dist/assets/de-CBla4hjd.js +0 -14
- package/dist/assets/es-BmnZtxTD.js +0 -20
- package/dist/assets/fileTreeStatus-BSHFIGEg.js +0 -1
- package/dist/assets/fr-OVKIBUZO.js +0 -18
- package/dist/assets/index-CKCFKmjF.js +0 -1
- package/dist/assets/index-Dbx4V4GN.css +0 -1
- package/dist/assets/index-vQ1Pg1cr.js +0 -61
- package/dist/assets/ja-6v-yr-gE.js +0 -20
- package/dist/assets/ko-I_ZLvgQ9.js +0 -20
- package/dist/assets/main-CmO4dRtR.js +0 -2
- package/dist/assets/main-b9vqhlYM.js +0 -999
- package/dist/assets/markdownCore-BPEZRQfP.js +0 -7
- package/dist/assets/miniChat-B5Oi7B-6.js +0 -2
- package/dist/assets/mobile-CVtShif9.js +0 -2
- package/dist/assets/nl-DcvIJf46.js +0 -20
- package/dist/assets/patchFileDiff-CW3wCH1O.js +0 -13
- package/dist/assets/pl-JxCUl0sd.js +0 -20
- package/dist/assets/pt-BR-L89mjja2.js +0 -20
- package/dist/assets/relocateRestoredSession-DYh_9oBa.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-BD-1JHSg.js +0 -2
- package/dist/assets/renderMobileApp-7s5InXt1.js +0 -25
- package/dist/assets/runtimeEndpointReset-DqLJC6L1.js +0 -53
- package/dist/assets/shikiHighlight-CXgZnvHh.js +0 -1
- package/dist/assets/space-apply-1trGVIl8.js +0 -1
- package/dist/assets/space-close-y7j-NuBv.js +0 -1
- package/dist/assets/tr-ka5J1flt.js +0 -11
- package/dist/assets/uk-PyGe6Q_u.js +0 -20
- package/dist/assets/useInlineCommentController-CrjSGCpi.js +0 -2
- package/dist/assets/vendor-codemirror-lang-python-B7ClrMIT.js +0 -3
- package/dist/assets/vendor-codemirror-lang-xml-C8tGnS64.js +0 -1
- package/dist/assets/vendor-codemirror-language-data-q2bKO1EM.js +0 -2
- package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +0 -1
- package/dist/assets/vendor-katex-CHD9BdDO.js +0 -257
- package/dist/assets/vendor-katex-Cp579UoJ.css +0 -1
- package/dist/assets/zh-CN-B0P2tybJ.js +0 -20
- package/dist/assets/zh-TW-Bs9C5cN8.js +0 -20
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Try to preserve cache usage". A provider keeps a prompt cache per model, so
|
|
3
|
+
* switching models mid-session makes the new model read the whole context
|
|
4
|
+
* uncached, and the old model's cache goes cold while the session is away from
|
|
5
|
+
* it. While the model that wrote the last answer likely still holds a warm
|
|
6
|
+
* cache, Auto stays on it instead of moving down to a lower category; moving up
|
|
7
|
+
* is never held back.
|
|
8
|
+
*
|
|
9
|
+
* "Lower" is the built-in category order: trivial < research < implement <
|
|
10
|
+
* hard. Categories the user added have no place in that order, so a send routed
|
|
11
|
+
* to one of them switches as it always did.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { BUILTIN_CATEGORIES } from './defaults.js';
|
|
15
|
+
|
|
16
|
+
/** How long a provider keeps a prompt cache after its last use. */
|
|
17
|
+
export const PROMPT_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
18
|
+
|
|
19
|
+
/** OpenCode's warming `duration` when the config says only `true` (core `plugin/warming.ts`). */
|
|
20
|
+
const DEFAULT_WARMING_DURATION_MS = 30 * 60 * 1000;
|
|
21
|
+
|
|
22
|
+
const RANK = new Map(BUILTIN_CATEGORIES.map((category, index) => [category.id, index]));
|
|
23
|
+
|
|
24
|
+
const rankOf = (category) => (category.builtin ? RANK.get(category.id) ?? null : null);
|
|
25
|
+
|
|
26
|
+
const sameModel = (a, b) => a.providerID === b.providerID && a.modelID === b.modelID;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The category Auto keeps instead of `chosen`, or null to switch as Jev said.
|
|
30
|
+
* `current` is the model that wrote the session's last answer. Several
|
|
31
|
+
* categories can share that model; the highest of them is where the session
|
|
32
|
+
* is, so a choice below it is a move down. Only a category that runs the same
|
|
33
|
+
* agent as `chosen` is kept: another agent sends another system prompt, which
|
|
34
|
+
* misses the cache anyway, and would run the request with tools it did not ask
|
|
35
|
+
* for (a planning agent for an edit).
|
|
36
|
+
*/
|
|
37
|
+
export const cacheHoldCategory = ({ categories, fallback, composerAgent, chosen, current }) => {
|
|
38
|
+
const chosenRank = chosen ? rankOf(chosen) : null;
|
|
39
|
+
if (chosenRank === null || !current) return null;
|
|
40
|
+
const modelOf = (category) => category.model ?? fallback.model;
|
|
41
|
+
const agentOf = (category) => category.agent || composerAgent || null;
|
|
42
|
+
if (sameModel(modelOf(chosen), current)) return null;
|
|
43
|
+
let held = null;
|
|
44
|
+
for (const category of categories) {
|
|
45
|
+
const rank = rankOf(category);
|
|
46
|
+
if (rank === null || !sameModel(modelOf(category), current) || agentOf(category) !== agentOf(chosen)) continue;
|
|
47
|
+
if (!held || rank > rankOf(held)) held = category;
|
|
48
|
+
}
|
|
49
|
+
return held && rankOf(held) > chosenRank ? held : null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const DURATION = /^(\d+(?:\.\d+)?)\s*([a-z]+)$/i;
|
|
53
|
+
const UNIT_MS = {
|
|
54
|
+
ms: 1, millis: 1, millisecond: 1, milliseconds: 1,
|
|
55
|
+
s: 1000, second: 1000, seconds: 1000,
|
|
56
|
+
m: 60_000, minute: 60_000, minutes: 60_000,
|
|
57
|
+
h: 3_600_000, hour: 3_600_000, hours: 3_600_000,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** OpenCode answers a duration as `"1800000 millis"`; a hand-written `"30 minutes"` reads too. */
|
|
61
|
+
const durationMs = (text) => {
|
|
62
|
+
const match = DURATION.exec(text.trim());
|
|
63
|
+
const unit = match ? UNIT_MS[match[2].toLowerCase()] : undefined;
|
|
64
|
+
const ms = unit ? Number(match[1]) * unit : NaN;
|
|
65
|
+
return Number.isFinite(ms) && ms > 0 ? ms : null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const warmingSchema = z.union([z.boolean(), z.object({ duration: z.string().optional() })]);
|
|
69
|
+
const configEntriesSchema = z.array(z.object({ info: z.object({ warming: warmingSchema.optional() }).nullish() }));
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* How long after its last answer a session's cache stays warm, from the config
|
|
73
|
+
* entries OpenCode answers for the directory (lowest priority first, the last
|
|
74
|
+
* one that sets `warming` wins, as OpenCode reads it). With warming on,
|
|
75
|
+
* OpenCode pings the model until `duration` after the last turn, and the last
|
|
76
|
+
* ping keeps the cache one more lifetime. Entries this cannot read count as
|
|
77
|
+
* warming off: holding a model on a cache that went cold is the costlier
|
|
78
|
+
* mistake.
|
|
79
|
+
*/
|
|
80
|
+
export const warmWindowMs = (configEntries) => {
|
|
81
|
+
const parsed = configEntriesSchema.safeParse(configEntries);
|
|
82
|
+
if (!parsed.success) return PROMPT_CACHE_TTL_MS;
|
|
83
|
+
const warming = parsed.data.findLast((entry) => entry.info?.warming !== undefined)?.info.warming;
|
|
84
|
+
if (!warming) return PROMPT_CACHE_TTL_MS;
|
|
85
|
+
const duration = warming === true || !warming.duration ? null : durationMs(warming.duration);
|
|
86
|
+
return (duration ?? DEFAULT_WARMING_DURATION_MS) + PROMPT_CACHE_TTL_MS;
|
|
87
|
+
};
|
|
@@ -79,11 +79,20 @@ export const JEV_TIMEOUT_MS = 4000;
|
|
|
79
79
|
* request refers to, and that finished work carries no weight, put them at
|
|
80
80
|
* 97% and ~75% trivial while "ok, do it" after a plan stayed implement and a
|
|
81
81
|
* regression report stayed hard (jev-router lab, test/follow-up-instructions.js).
|
|
82
|
+
*
|
|
83
|
+
* The fourth line covers a bare approval, which the third would otherwise
|
|
84
|
+
* read as trivial: "go" after a plan carries no work of its own, so it takes
|
|
85
|
+
* the category of the work it approves (the #4374 author measured this on
|
|
86
|
+
* their own prompts; not yet replayed in the jev-router lab).
|
|
87
|
+
* It does not let finished work raise a follow-up; a follow-up that only
|
|
88
|
+
* looks cheap because it reuses a warm context is the router's concern, not
|
|
89
|
+
* the classifier's.
|
|
82
90
|
*/
|
|
83
91
|
export const ROUTING_INSTRUCTIONS = [
|
|
84
92
|
'Pick the task category that best describes this coding request, judging the reasoning it demands rather than the length of the reply it asks for.',
|
|
85
93
|
'A request that wants a one-line answer to a hard debugging or design question is still hard; a long but mechanical edit is still trivial.',
|
|
86
94
|
'The thing to classify is `request`, the latest user message. Read `history` (earlier conversation, oldest first) only to understand what `request` refers to, then judge the reasoning `request` itself demands. Do not let the difficulty of work already done in `history` raise or lower the category.',
|
|
95
|
+
'A bare approval such as "go", "yes", "approve", or "do it" authorises the work just described in history: classify it as the category that work demands, not as trivial.',
|
|
87
96
|
];
|
|
88
97
|
|
|
89
98
|
/**
|
|
@@ -139,4 +148,6 @@ export const BUILTIN_CATEGORIES = [
|
|
|
139
148
|
];
|
|
140
149
|
|
|
141
150
|
export const DEFAULT_MIN_CONFIDENCE = 0.6;
|
|
151
|
+
/** "Try to preserve cache usage" (`cache.js`) is on unless the user turned it off. */
|
|
152
|
+
export const DEFAULT_PRESERVE_CACHE = true;
|
|
142
153
|
export const DEFAULT_SAFETY_THRESHOLD = 0.6;
|
|
@@ -23,12 +23,20 @@ export const turnsToHistory = (turns, limits = HISTORY_LIMITS) => {
|
|
|
23
23
|
return history;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
/** The model that wrote a settled answer and when it finished, or null when the record lacks either. */
|
|
27
|
+
const answeredBy = (message) => (message?.providerID && message.modelID && message.completed
|
|
28
|
+
? { providerID: message.providerID, modelID: message.modelID, completed: message.completed }
|
|
29
|
+
: null);
|
|
30
|
+
|
|
26
31
|
/**
|
|
27
|
-
*
|
|
28
|
-
* interrupted mid-turn)
|
|
29
|
-
*
|
|
32
|
+
* `history` is empty and `lastAnswer` null when the session has no settled
|
|
33
|
+
* answer yet (a new session, or one interrupted mid-turn): routing then judges
|
|
34
|
+
* the request on its own and has no cache to preserve. A read failure is
|
|
35
|
+
* thrown so the caller can decide; it is not an empty history.
|
|
30
36
|
*/
|
|
31
37
|
export const loadRoutingHistory = async ({ readPage, signal }) => {
|
|
32
38
|
const context = await loadAssistContext({ readPage, signal });
|
|
33
|
-
return context
|
|
39
|
+
return context
|
|
40
|
+
? { history: turnsToHistory(context.turns), lastAnswer: answeredBy(context.last) }
|
|
41
|
+
: { history: [], lastAnswer: null };
|
|
34
42
|
};
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
resolveClassifier,
|
|
27
27
|
} from './classifier.js';
|
|
28
28
|
import { loadRoutingHistory } from './history.js';
|
|
29
|
+
import { PROMPT_CACHE_TTL_MS, cacheHoldCategory, warmWindowMs } from './cache.js';
|
|
29
30
|
import { readOpenCodeCredentials } from '../opencode/auth.js';
|
|
30
31
|
import { ENTERPRISE_MODE_ERROR, isEnterpriseMode } from '../enterprise-mode.js';
|
|
31
32
|
|
|
@@ -122,6 +123,8 @@ export function createRoutingRuntime({
|
|
|
122
123
|
enterpriseMode = isEnterpriseMode,
|
|
123
124
|
readPinnedEndpoint = readPinnedCustomEndpoint,
|
|
124
125
|
listCatalogModels = null,
|
|
126
|
+
readSessionHistory = null,
|
|
127
|
+
readConfigEntries = null,
|
|
125
128
|
now = Date.now,
|
|
126
129
|
}) {
|
|
127
130
|
const permissionDecisions = new Map();
|
|
@@ -220,7 +223,7 @@ export function createRoutingRuntime({
|
|
|
220
223
|
return OpenCode.make({ baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''), headers });
|
|
221
224
|
};
|
|
222
225
|
|
|
223
|
-
const readHistory = async ({ sessionId, directory }) => {
|
|
226
|
+
const readHistory = readSessionHistory ?? (async ({ sessionId, directory }) => {
|
|
224
227
|
const client = openCodeClient(directory);
|
|
225
228
|
const signal = AbortSignal.timeout(HISTORY_TIMEOUT_MS);
|
|
226
229
|
return loadRoutingHistory({
|
|
@@ -231,6 +234,31 @@ export function createRoutingRuntime({
|
|
|
231
234
|
{ signal },
|
|
232
235
|
),
|
|
233
236
|
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// v2 answers the effective config as its documents, lowest priority first.
|
|
240
|
+
const readConfig = readConfigEntries ?? ((directory) => openCodeClient(directory).config.get());
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* "Try to preserve cache usage": the category to stay on instead of a lower
|
|
244
|
+
* one while the model that wrote the last answer likely still holds a warm
|
|
245
|
+
* prompt cache, or null to switch as Jev said (`cache.js`). OpenCode's
|
|
246
|
+
* warming config is read only once the plain cache lifetime has passed.
|
|
247
|
+
*/
|
|
248
|
+
const cacheHold = async ({ config, chosen, composerAgent, lastAnswer, directory }) => {
|
|
249
|
+
if (!config.preserveCache || !lastAnswer) return null;
|
|
250
|
+
const held = cacheHoldCategory({
|
|
251
|
+
categories: enabledCategories(config), fallback: config.fallback, composerAgent, chosen, current: lastAnswer,
|
|
252
|
+
});
|
|
253
|
+
if (!held) return null;
|
|
254
|
+
const idle = now() - lastAnswer.completed;
|
|
255
|
+
if (idle < PROMPT_CACHE_TTL_MS) return held;
|
|
256
|
+
try {
|
|
257
|
+
return idle < warmWindowMs(await readConfig(directory)) ? held : null;
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.warn('[routing] OpenCode config unavailable, treating the prompt cache as cold:', errorMessage(error));
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
234
262
|
};
|
|
235
263
|
|
|
236
264
|
// A category without a model of its own means "the fallback pair"; a variant
|
|
@@ -300,8 +328,9 @@ export function createRoutingRuntime({
|
|
|
300
328
|
let selection;
|
|
301
329
|
if (state.autoReady) {
|
|
302
330
|
let history = [];
|
|
331
|
+
let lastAnswer = null;
|
|
303
332
|
try {
|
|
304
|
-
history = await readHistory({ sessionId, directory });
|
|
333
|
+
({ history, lastAnswer } = await readHistory({ sessionId, directory }));
|
|
305
334
|
} catch (error) {
|
|
306
335
|
console.warn('[routing] history unavailable, routing on the request alone:', errorMessage(error));
|
|
307
336
|
}
|
|
@@ -315,7 +344,14 @@ export function createRoutingRuntime({
|
|
|
315
344
|
decision.confidence = result.confidence;
|
|
316
345
|
decision.reason = result.reason;
|
|
317
346
|
decision.ms = ms;
|
|
318
|
-
|
|
347
|
+
const held = await cacheHold({ config, chosen: result.category, composerAgent: agent, lastAnswer, directory });
|
|
348
|
+
if (held) {
|
|
349
|
+
// The send runs as the category the session is already on;
|
|
350
|
+
// `cacheHold.requested` keeps what Jev asked for.
|
|
351
|
+
decision.category = held.id;
|
|
352
|
+
decision.cacheHold = { requested: result.category.id };
|
|
353
|
+
}
|
|
354
|
+
selection = chooseSelection(config, held ?? result.category, agent);
|
|
319
355
|
} catch (error) {
|
|
320
356
|
decision.reason = 'error';
|
|
321
357
|
decision.error = errorMessage(error);
|
|
@@ -19,7 +19,10 @@ const readyConfig = () => {
|
|
|
19
19
|
return config;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
const makeRuntime = ({
|
|
22
|
+
const makeRuntime = ({
|
|
23
|
+
config = readyConfig(), token = 'key', classifierSource = null, customEndpoint = null, providerKeys = {}, zenPromotionActive = true,
|
|
24
|
+
enterprise = false, pinned = null, catalog = [], answers, askError, lastAnswer = null, configEntries = [], nowMs = 1_000_000_000,
|
|
25
|
+
} = {}) => {
|
|
23
26
|
const events = [];
|
|
24
27
|
const store = {
|
|
25
28
|
readConfig: vi.fn(async () => config),
|
|
@@ -34,6 +37,10 @@ const makeRuntime = ({ config = readyConfig(), token = 'key', classifierSource =
|
|
|
34
37
|
clearCustomEndpoint: vi.fn(async () => undefined),
|
|
35
38
|
};
|
|
36
39
|
const jev = { ask: vi.fn(async () => { if (askError) throw askError; return { answers, ms: 12 }; }) };
|
|
40
|
+
const readConfigEntries = vi.fn(async () => {
|
|
41
|
+
if (configEntries instanceof Error) throw configEntries;
|
|
42
|
+
return configEntries;
|
|
43
|
+
});
|
|
37
44
|
const runtime = createRoutingRuntime({
|
|
38
45
|
dataDir: '/unused',
|
|
39
46
|
buildOpenCodeUrl: () => 'http://127.0.0.1:1/',
|
|
@@ -49,8 +56,11 @@ const makeRuntime = ({ config = readyConfig(), token = 'key', classifierSource =
|
|
|
49
56
|
if (catalog instanceof Error) throw catalog;
|
|
50
57
|
return catalog;
|
|
51
58
|
},
|
|
59
|
+
readSessionHistory: async () => ({ history: [], lastAnswer }),
|
|
60
|
+
readConfigEntries,
|
|
61
|
+
now: () => nowMs,
|
|
52
62
|
});
|
|
53
|
-
return { runtime, store, jev, events };
|
|
63
|
+
return { runtime, store, jev, events, readConfigEntries };
|
|
54
64
|
};
|
|
55
65
|
|
|
56
66
|
describe('requestTextOf', () => {
|
|
@@ -173,6 +183,75 @@ describe('resolveAutoSelection', () => {
|
|
|
173
183
|
});
|
|
174
184
|
});
|
|
175
185
|
|
|
186
|
+
describe('preserving the prompt cache', () => {
|
|
187
|
+
const NOW = 1_000_000_000;
|
|
188
|
+
const MINUTE = 60_000;
|
|
189
|
+
const HARD = { providerID: 'openai', modelID: 'gpt-6-astra' };
|
|
190
|
+
// Hard on its own model and the composer's agent, so only the model differs.
|
|
191
|
+
const cacheConfig = () => {
|
|
192
|
+
const config = readyConfig();
|
|
193
|
+
config.categories = config.categories.map((c) => (c.id === 'hard' ? { ...c, agent: null } : c));
|
|
194
|
+
return config;
|
|
195
|
+
};
|
|
196
|
+
const answeredBy = (model, minutesAgo) => ({ ...model, completed: NOW - minutesAgo * MINUTE });
|
|
197
|
+
const route = async ({ choice, config = cacheConfig(), lastAnswer, configEntries, agent = 'build' }) => {
|
|
198
|
+
const made = makeRuntime({ config, lastAnswer, configEntries, nowMs: NOW, answers: { category: { choice, confidence: 0.95 } } });
|
|
199
|
+
const resolved = await made.runtime.resolveAutoSelection({ sessionId: 's1', model: AUTO, agent, requestText: 'push it' });
|
|
200
|
+
return { ...made, resolved };
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
it('stays on the warm model instead of moving down, and records what Jev asked for', async () => {
|
|
204
|
+
const { resolved, readConfigEntries } = await route({ choice: 'trivial', lastAnswer: answeredBy(HARD, 2) });
|
|
205
|
+
expect(resolved.model).toEqual({ providerID: 'openai', id: 'gpt-6-astra', variant: 'high' });
|
|
206
|
+
expect(resolved.agent).toBe('build');
|
|
207
|
+
expect(resolved.decision).toMatchObject({ category: 'hard', reason: 'routed', cacheHold: { requested: 'trivial' } });
|
|
208
|
+
expect(readConfigEntries).not.toHaveBeenCalled();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('moves down once the cache has expired, and moves up at any time', async () => {
|
|
212
|
+
const cold = await route({ choice: 'trivial', lastAnswer: answeredBy(HARD, 6) });
|
|
213
|
+
expect(cold.resolved.model).toMatchObject({ providerID: 'anthropic', id: 'claude-sonnet-5' });
|
|
214
|
+
expect(cold.resolved.decision.cacheHold).toBeUndefined();
|
|
215
|
+
|
|
216
|
+
const up = await route({ choice: 'hard', lastAnswer: answeredBy(FALLBACK.model, 1) });
|
|
217
|
+
expect(up.resolved.model).toMatchObject({ providerID: 'openai', id: 'gpt-6-astra' });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("counts OpenCode's session warming: its duration plus one cache lifetime", async () => {
|
|
221
|
+
const warming = [{ info: { model: 'anthropic/claude-sonnet-5' } }, { info: { warming: true } }];
|
|
222
|
+
expect((await route({ choice: 'trivial', lastAnswer: answeredBy(HARD, 20), configEntries: warming })).resolved.decision.category).toBe('hard');
|
|
223
|
+
expect((await route({ choice: 'trivial', lastAnswer: answeredBy(HARD, 36), configEntries: warming })).resolved.decision.category).toBe('trivial');
|
|
224
|
+
|
|
225
|
+
const hour = [{ info: { warming: { duration: '3600000 millis' } } }];
|
|
226
|
+
expect((await route({ choice: 'trivial', lastAnswer: answeredBy(HARD, 50), configEntries: hour })).resolved.decision.category).toBe('hard');
|
|
227
|
+
|
|
228
|
+
const turnedOff = [{ info: { warming: true } }, { info: { warming: false } }];
|
|
229
|
+
expect((await route({ choice: 'trivial', lastAnswer: answeredBy(HARD, 20), configEntries: turnedOff })).resolved.decision.category).toBe('trivial');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('treats the cache as cold when the OpenCode config cannot be read', async () => {
|
|
233
|
+
const { resolved } = await route({ choice: 'trivial', lastAnswer: answeredBy(HARD, 20), configEntries: new Error('OpenCode down') });
|
|
234
|
+
expect(resolved.decision.category).toBe('trivial');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('switches as before with the setting off, for a category of the user, or when the agent would change', async () => {
|
|
238
|
+
const off = cacheConfig();
|
|
239
|
+
off.preserveCache = false;
|
|
240
|
+
expect((await route({ choice: 'trivial', config: off, lastAnswer: answeredBy(HARD, 1) })).resolved.decision.category).toBe('trivial');
|
|
241
|
+
|
|
242
|
+
const own = cacheConfig();
|
|
243
|
+
own.categories.push({ id: 'docs', builtin: false, enabled: true, name: 'Docs', description: 'Docs', model: null, variant: null, agent: null });
|
|
244
|
+
expect((await route({ choice: 'docs', config: own, lastAnswer: answeredBy(HARD, 1) })).resolved.decision.category).toBe('docs');
|
|
245
|
+
|
|
246
|
+
// readyConfig's hard category runs the plan agent.
|
|
247
|
+
expect((await route({ choice: 'trivial', config: readyConfig(), lastAnswer: answeredBy(HARD, 1) })).resolved.decision.category).toBe('trivial');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('switches when the session has no settled answer to have warmed a cache', async () => {
|
|
251
|
+
expect((await route({ choice: 'trivial', lastAnswer: null })).resolved.decision.category).toBe('trivial');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
176
255
|
describe('jev endpoint', () => {
|
|
177
256
|
const capture = async (source, keys = {}) => {
|
|
178
257
|
let call = null;
|
|
@@ -20,6 +20,7 @@ import { z } from 'zod';
|
|
|
20
20
|
import {
|
|
21
21
|
BUILTIN_CATEGORIES,
|
|
22
22
|
DEFAULT_MIN_CONFIDENCE,
|
|
23
|
+
DEFAULT_PRESERVE_CACHE,
|
|
23
24
|
DEFAULT_SAFETY_THRESHOLD,
|
|
24
25
|
THINKING_LEVELS,
|
|
25
26
|
isAutoModel,
|
|
@@ -66,6 +67,7 @@ const fileSchema = z.object({
|
|
|
66
67
|
enabled: z.boolean().optional(),
|
|
67
68
|
fallback: z.object({ model: modelSchema, variant: variantSchema.optional() }).strict().nullable().optional(),
|
|
68
69
|
minConfidence: z.number().min(0).max(1).optional(),
|
|
70
|
+
preserveCache: z.boolean().optional(),
|
|
69
71
|
safetyNet: z.object({ enabled: z.boolean(), threshold: z.number().min(0).max(1) }).strict().optional(),
|
|
70
72
|
categories: z.record(z.string().regex(CATEGORY_ID), z.discriminatedUnion('builtin', [builtinOverrideSchema, userCategorySchema])).optional(),
|
|
71
73
|
}).strict();
|
|
@@ -86,6 +88,8 @@ const effectiveConfigSchema = z.object({
|
|
|
86
88
|
enabled: z.boolean(),
|
|
87
89
|
fallback: z.object({ model: modelSchema, variant: variantSchema }).strict().nullable(),
|
|
88
90
|
minConfidence: z.number().min(0).max(1),
|
|
91
|
+
// Clients from before the setting send a config without it.
|
|
92
|
+
preserveCache: z.boolean().default(DEFAULT_PRESERVE_CACHE),
|
|
89
93
|
safetyNet: z.object({ enabled: z.boolean(), threshold: z.number().min(0).max(1) }).strict(),
|
|
90
94
|
categories: z.array(effectiveCategorySchema).max(32),
|
|
91
95
|
}).strict();
|
|
@@ -178,6 +182,7 @@ export const resolveEffectiveConfig = (stored) => {
|
|
|
178
182
|
enabled: file.enabled ?? false,
|
|
179
183
|
fallback: file.fallback ? { model: file.fallback.model, variant: file.fallback.variant ?? null } : null,
|
|
180
184
|
minConfidence: file.minConfidence ?? DEFAULT_MIN_CONFIDENCE,
|
|
185
|
+
preserveCache: file.preserveCache ?? DEFAULT_PRESERVE_CACHE,
|
|
181
186
|
safetyNet: file.safetyNet ?? { enabled: false, threshold: DEFAULT_SAFETY_THRESHOLD },
|
|
182
187
|
categories,
|
|
183
188
|
};
|
|
@@ -214,7 +219,13 @@ export const toStoredConfig = (config) => {
|
|
|
214
219
|
for (const builtin of BUILTIN_CATEGORIES) {
|
|
215
220
|
if (!present.has(builtin.id)) categories[builtin.id] = { builtin: true, deleted: true };
|
|
216
221
|
}
|
|
217
|
-
const stored = {
|
|
222
|
+
const stored = {
|
|
223
|
+
version: FILE_VERSION,
|
|
224
|
+
enabled: config.enabled,
|
|
225
|
+
minConfidence: config.minConfidence,
|
|
226
|
+
preserveCache: config.preserveCache,
|
|
227
|
+
safetyNet: config.safetyNet,
|
|
228
|
+
};
|
|
218
229
|
if (config.fallback) {
|
|
219
230
|
stored.fallback = { model: config.fallback.model };
|
|
220
231
|
if (config.fallback.variant) stored.fallback.variant = config.fallback.variant;
|
|
@@ -11,6 +11,7 @@ const sampleConfig = (patch = {}) => ({
|
|
|
11
11
|
enabled: true,
|
|
12
12
|
fallback: { model: { providerID: 'anthropic', modelID: 'claude-sonnet-5' }, variant: 'medium' },
|
|
13
13
|
minConfidence: 0.6,
|
|
14
|
+
preserveCache: true,
|
|
14
15
|
safetyNet: { enabled: true, threshold: 0.6 },
|
|
15
16
|
categories: resolveEffectiveConfig(null).categories,
|
|
16
17
|
...patch,
|
|
@@ -44,6 +45,18 @@ describe('routing store', () => {
|
|
|
44
45
|
expect(await store.readConfig()).toEqual(config);
|
|
45
46
|
});
|
|
46
47
|
|
|
48
|
+
it('keeps the cache setting on until it is turned off, also for clients that do not send it', async () => {
|
|
49
|
+
expect(resolveEffectiveConfig(null).preserveCache).toBe(true);
|
|
50
|
+
expect(resolveEffectiveConfig({ version: 1, enabled: true }).preserveCache).toBe(true);
|
|
51
|
+
|
|
52
|
+
const store = createRoutingStore({ dataDir: await tempDir() });
|
|
53
|
+
await store.writeConfig(sampleConfig({ preserveCache: false }));
|
|
54
|
+
expect((await store.readConfig()).preserveCache).toBe(false);
|
|
55
|
+
|
|
56
|
+
const { preserveCache: _omitted, ...older } = sampleConfig();
|
|
57
|
+
expect(parseEffectiveConfig(older).preserveCache).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
47
60
|
it('records a removed built-in as deleted and keeps the others', async () => {
|
|
48
61
|
const config = sampleConfig();
|
|
49
62
|
config.categories = config.categories.filter((c) => c.id !== 'research');
|
|
@@ -100,6 +100,55 @@ Manual `runNow` does not claim a schedule occurrence. It also runs paused
|
|
|
100
100
|
(`enabled: false`) tasks — that is the point of the button — while scheduled
|
|
101
101
|
dispatches still skip disabled tasks, and completion never re-arms a paused task.
|
|
102
102
|
|
|
103
|
+
## Existing-session tasks
|
|
104
|
+
|
|
105
|
+
`targetSessionId` sends to an existing session instead of creating one.
|
|
106
|
+
`existing-session.js` validates changed or enabled targets on save, and all
|
|
107
|
+
targets on admission and send. A paused task can keep a stale target. The
|
|
108
|
+
target must be active and belong to the task project or its linked worktree.
|
|
109
|
+
Another registered project owns its exact checkout. Chats targets must remain
|
|
110
|
+
inside the chats scope. Missing targets return 404; archived or foreign targets
|
|
111
|
+
return 409, including through Run now.
|
|
112
|
+
|
|
113
|
+
The prompt enters the ordinary server message queue with `scheduledTask`
|
|
114
|
+
provenance. One project and task can have at most one waiting prompt. Run now
|
|
115
|
+
on an already queued prompt records `skipped` and returns 409 with
|
|
116
|
+
`busy: queued`. Admission sets `lastSessionId` to the target. Disable, delete
|
|
117
|
+
and retarget cancel waiting prompts, but cannot recall a send already in flight.
|
|
118
|
+
Saving an already-paused task with the same target keeps a prompt admitted by
|
|
119
|
+
Run now. Cancellation on disable applies only to an enabled-to-disabled change.
|
|
120
|
+
Scheduled once tasks are disabled after an admission attempt, including a
|
|
121
|
+
failed attempt. An admitted prompt stays queued. Run now can still admit a
|
|
122
|
+
paused task.
|
|
123
|
+
|
|
124
|
+
An overlapping admission attempt records `skipped` before the active admission
|
|
125
|
+
records `queued`. A later duplicate enqueue records `skipped` while the earlier
|
|
126
|
+
prompt remains waiting. The card shows the last attempt's status in that case.
|
|
127
|
+
|
|
128
|
+
Reuse status ends at queue send: `queued` becomes `sent`, `failed` or
|
|
129
|
+
`cancelled`. The existing task state fields hold the result. There is no
|
|
130
|
+
per-run history or assistant-completion tracking. `execution.useDefaults`
|
|
131
|
+
leaves the target model and agent unchanged. A pinned selection switches the
|
|
132
|
+
requested model and agent before sending. Tasks without a target keep the
|
|
133
|
+
new-session execution path below.
|
|
134
|
+
|
|
135
|
+
Reuse preserves the target's goal and permission auto-accept policy. Task
|
|
136
|
+
creation options for these policies are hidden in Existing mode and ignored
|
|
137
|
+
by the reuse runtime. Transient queue send failures use the ordinary retry
|
|
138
|
+
policy. Only definitive before-send target or task 404 or 409 refusals are
|
|
139
|
+
terminal, including removed or retargeted tasks and moved targets. Model or
|
|
140
|
+
prompt send errors use the ordinary retry path.
|
|
141
|
+
|
|
142
|
+
## New-session selection
|
|
143
|
+
|
|
144
|
+
A task either pins them (`execution.providerID`, `modelID`, `variant`, `agent`)
|
|
145
|
+
or sets `execution.useDefaults: true`. Such a task reads the session defaults
|
|
146
|
+
when each run starts (`session-defaults.js`: the project's `defaultModel` /
|
|
147
|
+
`defaultVariant` / `defaultAgent`, then the global ones), so changing a default
|
|
148
|
+
reaches it on its next run without editing it. With nothing set, the session is
|
|
149
|
+
created without a model or agent and OpenCode applies its own. A pinned value
|
|
150
|
+
stored next to `useDefaults` is kept only for when the task is pinned again.
|
|
151
|
+
|
|
103
152
|
## Files
|
|
104
153
|
|
|
105
154
|
- `packages/web/server/lib/scheduled-tasks/runtime.js`
|
|
@@ -193,7 +242,16 @@ project write lock on every `syncProject` when the project path is known:
|
|
|
193
242
|
name, schedule and execution. Enabling a loop in the UI (the loop-file
|
|
194
243
|
endpoint) records that approval; disabling withdraws it. A pull that changes
|
|
195
244
|
what a loop runs or when changes its fingerprint, and the loop waits for a
|
|
196
|
-
new approval. User-scope loops (`~/.agents/loops`) need none.
|
|
245
|
+
new approval. User-scope loops (`~/.agents/loops`) need none. A task the
|
|
246
|
+
gate holds back is returned with `loopApproval`: `required` (never enabled
|
|
247
|
+
here) or `outdated` (enabled, then the file changed); the UI explains the
|
|
248
|
+
pause from it. The field is never stored.
|
|
249
|
+
- **Agent tool and CLI.** `setEnabled` (behind `schedule.toggle` and
|
|
250
|
+
`openchamber schedule enable|disable`) changes a loop task the way the
|
|
251
|
+
Scheduled tasks checkbox does, through the loop file. It refuses (409) to
|
|
252
|
+
enable a paused project-scope loop: the approval is the user's decision in
|
|
253
|
+
Scheduled tasks, never an agent's or a script's. Disabling works for every
|
|
254
|
+
loop.
|
|
197
255
|
- **Loop-file mutations.** The loop file remains authoritative. The scheduled-
|
|
198
256
|
tasks UI opens it in the built-in file editor, updates its `enabled`
|
|
199
257
|
frontmatter through the loop-file endpoint, and deletes the file through the
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { OpenChamberControlError } from '../openchamber-control/error.js';
|
|
4
|
+
import { expandSnippets } from '../opencode/snippets.js';
|
|
5
|
+
|
|
6
|
+
export const createExistingSessionTasks = ({ projectConfigRuntime, messageQueueRuntime, createClient, listProjects, chatsScope, resolvePrimaryWorktreeRoot, isSessionArchived, emitTaskRunEvent }) => {
|
|
7
|
+
const admissions = new Set();
|
|
8
|
+
const validateTarget = async (projectId, targetSessionId) => {
|
|
9
|
+
if (!z.string().regex(/^[A-Za-z0-9_-]{4,128}$/).safeParse(targetSessionId).success) {
|
|
10
|
+
throw new OpenChamberControlError('targetSessionId is invalid', 400);
|
|
11
|
+
}
|
|
12
|
+
if (await isSessionArchived?.(targetSessionId)) throw new OpenChamberControlError('Scheduled target is archived', 409);
|
|
13
|
+
let session;
|
|
14
|
+
try {
|
|
15
|
+
session = await createClient('').session.get({ sessionID: targetSessionId });
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error?.name === 'NotFoundError' || error?.status === 404 || error?.statusCode === 404) throw new OpenChamberControlError('Scheduled target not found', 404);
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
if (!session?.id) throw new OpenChamberControlError('Scheduled target not found', 404);
|
|
21
|
+
if (session.id !== targetSessionId) throw new OpenChamberControlError('Scheduled target identity mismatch', 409);
|
|
22
|
+
if (session.time?.archived) throw new OpenChamberControlError('Scheduled target is archived', 409);
|
|
23
|
+
const projects = await listProjects();
|
|
24
|
+
const project = projects.find((entry) => entry.id === projectId);
|
|
25
|
+
const directory = session.location?.directory;
|
|
26
|
+
if (!directory) throw new OpenChamberControlError('Scheduled target has no directory', 409);
|
|
27
|
+
if (projects.some((entry) => entry.id !== projectId && path.resolve(entry.path) === path.resolve(directory))) {
|
|
28
|
+
throw new OpenChamberControlError('Scheduled target belongs to another project', 409);
|
|
29
|
+
}
|
|
30
|
+
let belongs = false;
|
|
31
|
+
if (chatsScope && projectId === chatsScope.id) belongs = chatsScope.contains(directory);
|
|
32
|
+
else if (project && directory) {
|
|
33
|
+
belongs = path.resolve(directory) === path.resolve(project.path);
|
|
34
|
+
if (!belongs && resolvePrimaryWorktreeRoot) {
|
|
35
|
+
const owner = await resolvePrimaryWorktreeRoot(directory);
|
|
36
|
+
belongs = Boolean(owner?.root) && path.resolve(owner.root) === path.resolve(project.path);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!belongs) throw new OpenChamberControlError('Scheduled target is outside the task project', 409);
|
|
40
|
+
return { session, directory };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const record = async (projectId, taskId, status, error, lastRunAt) => {
|
|
44
|
+
const patch = {
|
|
45
|
+
lastStatus: status, lastError: error || undefined, updatedAt: Date.now(),
|
|
46
|
+
};
|
|
47
|
+
if (lastRunAt !== undefined) patch.lastRunAt = lastRunAt;
|
|
48
|
+
const result = await projectConfigRuntime.updateScheduledTaskState(projectId, taskId, patch);
|
|
49
|
+
emitTaskRunEvent?.({ projectID: projectId, taskID: taskId, status, ranAt: Date.now(), sessionID: result.task?.state?.lastSessionId });
|
|
50
|
+
return result.task;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const run = async (projectId, task, reason, scheduledFor, nextRunAt) => {
|
|
54
|
+
const triggeredAt = Date.now();
|
|
55
|
+
const key = `${projectId}:${task.id}`;
|
|
56
|
+
if (admissions.has(key)) {
|
|
57
|
+
try {
|
|
58
|
+
await record(projectId, task.id, 'skipped', 'task is already queued');
|
|
59
|
+
} catch (persistError) {
|
|
60
|
+
console.warn('[scheduled-tasks] admission failure state write failed:', persistError?.message);
|
|
61
|
+
}
|
|
62
|
+
return { ok: false, queued: true, error: 'task is already queued' };
|
|
63
|
+
}
|
|
64
|
+
admissions.add(key);
|
|
65
|
+
let admitted = false;
|
|
66
|
+
try {
|
|
67
|
+
if (reason === 'scheduled') {
|
|
68
|
+
const claim = await projectConfigRuntime.updateScheduledTaskStateIf(projectId, task.id,
|
|
69
|
+
(candidate) => candidate.enabled && (!Number.isFinite(candidate.state?.lastScheduledFor) || Math.abs(candidate.state.lastScheduledFor - scheduledFor) > 5_000),
|
|
70
|
+
{ lastScheduledFor: scheduledFor, nextRunAt: nextRunAt ?? undefined });
|
|
71
|
+
if (!claim.updated) return { ok: false, skipped: true };
|
|
72
|
+
}
|
|
73
|
+
const { directory } = await validateTarget(projectId, task.targetSessionId);
|
|
74
|
+
const sendConfig = task.execution.useDefaults ? {} : {
|
|
75
|
+
providerID: task.execution.providerID, modelID: task.execution.modelID,
|
|
76
|
+
agent: task.execution.agent, variant: task.execution.variant,
|
|
77
|
+
};
|
|
78
|
+
const scheduledTask = { projectId, taskId: task.id };
|
|
79
|
+
await messageQueueRuntime.enqueue(task.targetSessionId, directory, {
|
|
80
|
+
content: task.execution.prompt,
|
|
81
|
+
text: expandSnippets(task.execution.prompt, directory),
|
|
82
|
+
sendConfig, scheduledTask,
|
|
83
|
+
});
|
|
84
|
+
admitted = true;
|
|
85
|
+
const result = await projectConfigRuntime.updateScheduledTaskState(projectId, task.id, {
|
|
86
|
+
lastRunAt: triggeredAt, lastStatus: 'queued', lastError: undefined,
|
|
87
|
+
lastSessionId: task.targetSessionId, updatedAt: Date.now(),
|
|
88
|
+
});
|
|
89
|
+
emitTaskRunEvent?.({ projectID: projectId, taskID: task.id, status: 'queued', ranAt: Date.now(), sessionID: task.targetSessionId });
|
|
90
|
+
return { ok: true, task: result.task, sessionID: task.targetSessionId, directory };
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (admitted) await messageQueueRuntime.cancelScheduledTask(projectId, task.id);
|
|
93
|
+
const queued = error?.status === 409 && error.message === 'task is already queued';
|
|
94
|
+
let updated = task;
|
|
95
|
+
try {
|
|
96
|
+
updated = await record(projectId, task.id, queued ? 'skipped' : 'failed', error.message, triggeredAt);
|
|
97
|
+
} catch (persistError) {
|
|
98
|
+
console.warn('[scheduled-tasks] admission failure state write failed:', persistError?.message);
|
|
99
|
+
}
|
|
100
|
+
return { ok: false, queued, error: error.message, statusCode: error.statusCode ?? error.status, task: updated };
|
|
101
|
+
} finally {
|
|
102
|
+
admissions.delete(key);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const beforeSend = async (sessionId, directory, item) => {
|
|
107
|
+
const { projectId, taskId } = item.scheduledTask;
|
|
108
|
+
const task = (await projectConfigRuntime.listScheduledTasks(projectId)).find((entry) => entry.id === taskId);
|
|
109
|
+
if (!task || task.targetSessionId !== sessionId) throw new OpenChamberControlError('Scheduled task target changed or was removed', 409);
|
|
110
|
+
const target = await validateTarget(projectId, sessionId);
|
|
111
|
+
if (target.directory !== directory) throw new OpenChamberControlError('Scheduled target moved', 409);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
validateTarget, run, beforeSend,
|
|
116
|
+
result: ({ projectId, taskId }, status, error) => record(projectId, taskId, status, error),
|
|
117
|
+
cancel: (projectId, taskId) => messageQueueRuntime.cancelScheduledTask(projectId, taskId),
|
|
118
|
+
};
|
|
119
|
+
};
|