@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,350 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { OpenCode } from '@opencode/client';
|
|
3
|
+
import { createExistingSessionTasks } from './existing-session.js';
|
|
4
|
+
import { createScheduledTaskService } from './service.js';
|
|
5
|
+
import { registerScheduledTaskRoutes } from './routes.js';
|
|
6
|
+
import { createScheduledTasksRuntime } from './runtime.js';
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.unstubAllGlobals();
|
|
10
|
+
vi.useRealTimers();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const task = {
|
|
14
|
+
id: 'task-1', name: 'Repeat', enabled: true, targetSessionId: 'ses_target',
|
|
15
|
+
schedule: { kind: 'daily', time: '09:00' }, execution: { prompt: 'Follow up', useDefaults: true }, state: {},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const setup = (overrides = {}) => {
|
|
19
|
+
let stored = structuredClone(task);
|
|
20
|
+
const projectConfigRuntime = {
|
|
21
|
+
listScheduledTasks: async () => [stored],
|
|
22
|
+
updateScheduledTaskState: async (_project, _task, patch) => {
|
|
23
|
+
stored = { ...stored, state: { ...stored.state, ...patch } };
|
|
24
|
+
return { task: stored };
|
|
25
|
+
},
|
|
26
|
+
updateScheduledTaskStateIf: async (_project, _task, predicate, patch) => {
|
|
27
|
+
if (!predicate(stored)) return { task: stored, updated: false };
|
|
28
|
+
stored = { ...stored, state: { ...stored.state, ...patch } };
|
|
29
|
+
return { task: stored, updated: true };
|
|
30
|
+
},
|
|
31
|
+
upsertScheduledTask: async (_project, input) => {
|
|
32
|
+
stored = { ...stored, ...input };
|
|
33
|
+
return { task: stored };
|
|
34
|
+
},
|
|
35
|
+
deleteScheduledTask: async () => ({ deleted: true }),
|
|
36
|
+
};
|
|
37
|
+
const messageQueueRuntime = { enqueue: vi.fn(async () => {}), cancelScheduledTask: vi.fn(async () => {}) };
|
|
38
|
+
const runtime = createExistingSessionTasks({
|
|
39
|
+
projectConfigRuntime, messageQueueRuntime,
|
|
40
|
+
createClient: () => ({ session: { get: async ({ sessionID }) => ({ id: sessionID, location: { directory: '/repo' } }) } }),
|
|
41
|
+
listProjects: async () => [{ id: 'project-1', path: '/repo' }],
|
|
42
|
+
...overrides,
|
|
43
|
+
});
|
|
44
|
+
return { runtime, projectConfigRuntime, messageQueueRuntime, stored: () => stored };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
describe('existing session scheduling', () => {
|
|
48
|
+
it('the scheduler reuses a target without creating a chat or reading new-session defaults', async () => {
|
|
49
|
+
const { projectConfigRuntime, messageQueueRuntime } = setup();
|
|
50
|
+
const fetchImpl = vi.fn(async () => Response.json({ location: { directory: '/repo' }, data: { id: 'ses_target', location: { directory: '/repo' } } }));
|
|
51
|
+
vi.stubGlobal('fetch', fetchImpl);
|
|
52
|
+
const readSessionDefaults = vi.fn();
|
|
53
|
+
const createChatDirectory = vi.fn();
|
|
54
|
+
const scheduler = createScheduledTasksRuntime({
|
|
55
|
+
projectConfigRuntime, messageQueueRuntime,
|
|
56
|
+
listProjects: async () => [{ id: 'project-1', path: '/repo' }],
|
|
57
|
+
chatsScope: { id: 'project-1', root: '/repo', contains: () => true, createChatDirectory },
|
|
58
|
+
buildOpenCodeUrl: (route) => `http://opencode.test${route}`, getOpenCodeAuthHeaders: () => ({}),
|
|
59
|
+
readSessionDefaults,
|
|
60
|
+
});
|
|
61
|
+
await scheduler.syncProject('project-1');
|
|
62
|
+
expect(await scheduler.runNow('project-1', 'task-1')).toMatchObject({ ok: true, sessionID: 'ses_target' });
|
|
63
|
+
expect(createChatDirectory).not.toHaveBeenCalled();
|
|
64
|
+
expect(readSessionDefaults).not.toHaveBeenCalled();
|
|
65
|
+
expect(fetchImpl).toHaveBeenCalledOnce();
|
|
66
|
+
expect(new URL(fetchImpl.mock.calls[0][0]).pathname).toBe('/api/session/ses_target');
|
|
67
|
+
});
|
|
68
|
+
it('admits to the ordinary queue, records the target, then finishes at send', async () => {
|
|
69
|
+
const { runtime, messageQueueRuntime, stored } = setup();
|
|
70
|
+
const result = await runtime.run('project-1', { ...task, execution: { ...task.execution, providerID: 'old', modelID: 'old', agent: 'old' } }, 'manual');
|
|
71
|
+
expect(result).toMatchObject({ ok: true, sessionID: 'ses_target', directory: '/repo' });
|
|
72
|
+
expect(messageQueueRuntime.enqueue).toHaveBeenCalledWith('ses_target', '/repo', {
|
|
73
|
+
content: 'Follow up', text: 'Follow up', sendConfig: {}, scheduledTask: { projectId: 'project-1', taskId: 'task-1' },
|
|
74
|
+
});
|
|
75
|
+
expect(stored().state).toMatchObject({ lastStatus: 'queued', lastSessionId: 'ses_target' });
|
|
76
|
+
await runtime.result({ projectId: 'project-1', taskId: 'task-1' }, 'sent');
|
|
77
|
+
expect(stored().state.lastStatus).toBe('sent');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('records a duplicate Run now as skipped with queued busy status', async () => {
|
|
81
|
+
const { runtime, messageQueueRuntime, stored } = setup();
|
|
82
|
+
messageQueueRuntime.enqueue.mockRejectedValue(Object.assign(new Error('task is already queued'), { status: 409 }));
|
|
83
|
+
expect(await runtime.run('project-1', task, 'manual')).toMatchObject({ ok: false, queued: true, statusCode: 409 });
|
|
84
|
+
expect(stored().state.lastStatus).toBe('skipped');
|
|
85
|
+
expect(messageQueueRuntime.cancelScheduledTask).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it.each(['manual', 'scheduled'])('records a %s attempt that overlaps an admission before the final queued write', async (reason) => {
|
|
89
|
+
let releaseTarget;
|
|
90
|
+
const target = new Promise((resolve) => { releaseTarget = resolve; });
|
|
91
|
+
const { runtime, projectConfigRuntime, stored, messageQueueRuntime } = setup({
|
|
92
|
+
createClient: () => ({ session: { get: () => target } }),
|
|
93
|
+
});
|
|
94
|
+
const claim = vi.spyOn(projectConfigRuntime, 'updateScheduledTaskStateIf');
|
|
95
|
+
const first = runtime.run('project-1', task, 'manual');
|
|
96
|
+
expect(await runtime.run('project-1', task, reason, Date.now())).toMatchObject({ ok: false, queued: true });
|
|
97
|
+
expect(stored().state).toMatchObject({ lastStatus: 'skipped', lastError: 'task is already queued' });
|
|
98
|
+
expect(claim).not.toHaveBeenCalled();
|
|
99
|
+
releaseTarget({ id: 'ses_target', location: { directory: '/repo' } });
|
|
100
|
+
expect(await first).toMatchObject({ ok: true });
|
|
101
|
+
expect(stored().state).toMatchObject({ lastStatus: 'queued', lastSessionId: 'ses_target' });
|
|
102
|
+
expect(messageQueueRuntime.enqueue).toHaveBeenCalledOnce();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it.each(['manual', 'scheduled'])('returns queued busy when the %s overlap state write rejects', async (reason) => {
|
|
106
|
+
let releaseTarget;
|
|
107
|
+
const target = new Promise((resolve) => { releaseTarget = resolve; });
|
|
108
|
+
const { runtime, projectConfigRuntime, messageQueueRuntime } = setup({
|
|
109
|
+
createClient: () => ({ session: { get: () => target } }),
|
|
110
|
+
});
|
|
111
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
112
|
+
const first = runtime.run('project-1', task, 'manual');
|
|
113
|
+
const write = vi.spyOn(projectConfigRuntime, 'updateScheduledTaskState').mockRejectedValueOnce(new Error('project lock timeout'));
|
|
114
|
+
try {
|
|
115
|
+
await expect(runtime.run('project-1', task, reason, Date.now())).resolves.toMatchObject({
|
|
116
|
+
ok: false, queued: true, error: 'task is already queued',
|
|
117
|
+
});
|
|
118
|
+
expect(write).toHaveBeenCalledWith('project-1', task.id, expect.objectContaining({ lastStatus: 'skipped' }));
|
|
119
|
+
expect(warn).toHaveBeenCalledWith('[scheduled-tasks] admission failure state write failed:', 'project lock timeout');
|
|
120
|
+
} finally {
|
|
121
|
+
releaseTarget({ id: 'ses_target', location: { directory: '/repo' } });
|
|
122
|
+
await first;
|
|
123
|
+
warn.mockRestore();
|
|
124
|
+
}
|
|
125
|
+
expect(messageQueueRuntime.enqueue).toHaveBeenCalledOnce();
|
|
126
|
+
expect(messageQueueRuntime.cancelScheduledTask).not.toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('keeps manual busy 409 and re-arms the scheduled timer when overlap state writes reject', async () => {
|
|
130
|
+
vi.useFakeTimers();
|
|
131
|
+
vi.setSystemTime(new Date('2026-10-07T08:00:00Z'));
|
|
132
|
+
const { projectConfigRuntime, messageQueueRuntime } = setup();
|
|
133
|
+
await projectConfigRuntime.upsertScheduledTask('project-1', {
|
|
134
|
+
...task, schedule: { kind: 'daily', time: '08:01', timezone: 'UTC' },
|
|
135
|
+
});
|
|
136
|
+
let releaseAdmission;
|
|
137
|
+
let enteredAdmission;
|
|
138
|
+
const admission = new Promise((resolve) => { releaseAdmission = resolve; });
|
|
139
|
+
const entered = new Promise((resolve) => { enteredAdmission = resolve; });
|
|
140
|
+
messageQueueRuntime.enqueue.mockImplementationOnce(() => {
|
|
141
|
+
enteredAdmission();
|
|
142
|
+
return admission;
|
|
143
|
+
});
|
|
144
|
+
vi.stubGlobal('fetch', async () => Response.json({ data: { id: 'ses_target', location: { directory: '/repo' } } }));
|
|
145
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
146
|
+
const schedulerWarn = vi.fn();
|
|
147
|
+
const scheduler = createScheduledTasksRuntime({
|
|
148
|
+
projectConfigRuntime, messageQueueRuntime, logger: { warn: schedulerWarn },
|
|
149
|
+
listProjects: async () => [{ id: 'project-1', path: '/repo' }],
|
|
150
|
+
chatsScope: { id: 'project-1', root: '/repo', contains: () => true },
|
|
151
|
+
buildOpenCodeUrl: (route) => `http://opencode.test${route}`, getOpenCodeAuthHeaders: () => ({}),
|
|
152
|
+
});
|
|
153
|
+
const service = createScheduledTaskService({
|
|
154
|
+
projectConfigRuntime, scheduledTasksRuntime: scheduler,
|
|
155
|
+
readSettingsFromDiskMigrated: async () => ({ projects: [{ id: 'project-1', path: '/repo' }] }), sanitizeProjects: (projects) => projects,
|
|
156
|
+
});
|
|
157
|
+
const handlers = new Map();
|
|
158
|
+
const app = Object.fromEntries(['get', 'put', 'delete', 'patch', 'post'].map((method) => [method, (route, handler) => handlers.set(`${method}:${route}`, handler)]));
|
|
159
|
+
registerScheduledTaskRoutes(app, { scheduledTaskService: service });
|
|
160
|
+
let first;
|
|
161
|
+
try {
|
|
162
|
+
await scheduler.start();
|
|
163
|
+
first = scheduler.runNow('project-1', task.id);
|
|
164
|
+
await entered;
|
|
165
|
+
vi.spyOn(projectConfigRuntime, 'updateScheduledTaskState')
|
|
166
|
+
.mockRejectedValueOnce(new Error('disk write failed'))
|
|
167
|
+
.mockRejectedValueOnce(new Error('project lock timeout'));
|
|
168
|
+
const response = { statusCode: 200, body: null, status(code) { this.statusCode = code; return this; }, json(body) { this.body = body; return this; } };
|
|
169
|
+
await handlers.get('post:/api/projects/:projectId/scheduled-tasks/:taskId/run')({ params: { projectId: 'project-1', taskId: task.id } }, response);
|
|
170
|
+
expect(response.statusCode).toBe(409);
|
|
171
|
+
expect(response.body).toMatchObject({ busy: 'queued', error: 'task is already queued' });
|
|
172
|
+
await vi.advanceTimersByTimeAsync(65_000);
|
|
173
|
+
expect(warn).toHaveBeenCalledWith('[scheduled-tasks] admission failure state write failed:', 'project lock timeout');
|
|
174
|
+
expect(schedulerWarn).not.toHaveBeenCalled();
|
|
175
|
+
expect(vi.getTimerCount()).toBe(1);
|
|
176
|
+
expect(messageQueueRuntime.enqueue).toHaveBeenCalledOnce();
|
|
177
|
+
releaseAdmission();
|
|
178
|
+
await expect(first).resolves.toMatchObject({ ok: true });
|
|
179
|
+
await vi.advanceTimersByTimeAsync(24 * 60 * 60 * 1000);
|
|
180
|
+
expect(messageQueueRuntime.enqueue).toHaveBeenCalledTimes(2);
|
|
181
|
+
expect(schedulerWarn).not.toHaveBeenCalled();
|
|
182
|
+
expect(vi.getTimerCount()).toBe(1);
|
|
183
|
+
} finally {
|
|
184
|
+
releaseAdmission();
|
|
185
|
+
await first;
|
|
186
|
+
scheduler.stop();
|
|
187
|
+
warn.mockRestore();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('ignores task goal and auto-accept options when reusing a target', async () => {
|
|
192
|
+
const { projectConfigRuntime, messageQueueRuntime } = setup();
|
|
193
|
+
const execution = { ...task.execution, goalEnabled: true, goalTokenBudget: 5000, permissionAutoAccept: true };
|
|
194
|
+
await projectConfigRuntime.upsertScheduledTask('project-1', { ...task, execution });
|
|
195
|
+
const fetchImpl = vi.fn(async () => Response.json({ data: { id: 'ses_target', location: { directory: '/repo' } } }));
|
|
196
|
+
vi.stubGlobal('fetch', fetchImpl);
|
|
197
|
+
const setSessionAutoAccept = vi.fn();
|
|
198
|
+
const persistSessionGoal = vi.fn();
|
|
199
|
+
const scheduler = createScheduledTasksRuntime({
|
|
200
|
+
projectConfigRuntime, messageQueueRuntime, setSessionAutoAccept, persistSessionGoal,
|
|
201
|
+
listProjects: async () => [{ id: 'project-1', path: '/repo' }],
|
|
202
|
+
chatsScope: { id: 'project-1', root: '/repo', contains: () => true },
|
|
203
|
+
buildOpenCodeUrl: (route) => `http://opencode.test${route}`, getOpenCodeAuthHeaders: () => ({}),
|
|
204
|
+
});
|
|
205
|
+
await scheduler.syncProject('project-1');
|
|
206
|
+
await scheduler.runNow('project-1', task.id);
|
|
207
|
+
const item = messageQueueRuntime.enqueue.mock.calls[0][2];
|
|
208
|
+
expect(item.scheduledTask).toEqual({ projectId: 'project-1', taskId: 'task-1' });
|
|
209
|
+
await scheduler.beforeScheduledTaskSend('ses_target', '/repo', item);
|
|
210
|
+
expect(setSessionAutoAccept).not.toHaveBeenCalled();
|
|
211
|
+
expect(persistSessionGoal).not.toHaveBeenCalled();
|
|
212
|
+
expect(fetchImpl.mock.calls.every(([, init]) => !init?.method || init.method === 'GET')).toBe(true);
|
|
213
|
+
expect(item.context).toBeUndefined();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('Run now on a deleted target responds 404 with its message', async () => {
|
|
217
|
+
const { runtime, projectConfigRuntime } = setup({ createClient: () => OpenCode.make({ baseUrl: 'http://opencode.test', fetch: async () => Response.json({ name: 'NotFoundError', message: 'Not found' }, { status: 404 }) }) });
|
|
218
|
+
const scheduledTasksRuntime = { runNow: (project, id) => runtime.run(project, { ...task, id }, 'manual') };
|
|
219
|
+
const service = createScheduledTaskService({
|
|
220
|
+
projectConfigRuntime, scheduledTasksRuntime,
|
|
221
|
+
readSettingsFromDiskMigrated: async () => ({ projects: [{ id: 'project-1', path: '/repo' }] }), sanitizeProjects: (projects) => projects,
|
|
222
|
+
});
|
|
223
|
+
const handlers = new Map();
|
|
224
|
+
const app = Object.fromEntries(['get', 'put', 'delete', 'patch', 'post'].map((method) => [method, (route, handler) => handlers.set(`${method}:${route}`, handler)]));
|
|
225
|
+
registerScheduledTaskRoutes(app, { scheduledTaskService: service });
|
|
226
|
+
const response = { statusCode: 200, body: null, status(code) { this.statusCode = code; return this; }, json(body) { this.body = body; return this; } };
|
|
227
|
+
await handlers.get('post:/api/projects/:projectId/scheduled-tasks/:taskId/run')({ params: { projectId: 'project-1', taskId: task.id } }, response);
|
|
228
|
+
expect(response.statusCode).toBe(404);
|
|
229
|
+
expect(response.body.error).toBe('Scheduled target not found');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('refuses archived and foreign targets and admits linked worktrees', async () => {
|
|
233
|
+
await expect(setup({ isSessionArchived: () => true }).runtime.validateTarget('project-1', 'ses_target')).rejects.toMatchObject({ statusCode: 409, message: 'Scheduled target is archived' });
|
|
234
|
+
const foreign = () => ({ session: { get: async () => ({ id: 'ses_target', location: { directory: '/worktree' } }) } });
|
|
235
|
+
await expect(setup({ createClient: foreign }).runtime.validateTarget('project-1', 'ses_target')).rejects.toMatchObject({ statusCode: 409 });
|
|
236
|
+
await expect(setup({ createClient: foreign, resolvePrimaryWorktreeRoot: async () => ({ root: '/repo' }) }).runtime.validateTarget('project-1', 'ses_target')).resolves.toMatchObject({ directory: '/worktree' });
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it.each(['removed', 'retargeted', 'moved'])('refuses a %s target/task before send with 409', async (change) => {
|
|
240
|
+
let directory = '/repo';
|
|
241
|
+
const { runtime, projectConfigRuntime, messageQueueRuntime } = setup({
|
|
242
|
+
createClient: () => ({ session: { get: async () => ({ id: 'ses_target', location: { directory } }) } }),
|
|
243
|
+
resolvePrimaryWorktreeRoot: async () => ({ root: '/repo' }),
|
|
244
|
+
});
|
|
245
|
+
await runtime.run('project-1', task, 'manual');
|
|
246
|
+
if (change === 'removed') projectConfigRuntime.listScheduledTasks = async () => [];
|
|
247
|
+
if (change === 'retargeted') await projectConfigRuntime.upsertScheduledTask('project-1', { ...task, targetSessionId: 'ses_another' });
|
|
248
|
+
if (change === 'moved') directory = '/worktree';
|
|
249
|
+
await expect(runtime.beforeSend('ses_target', '/repo', messageQueueRuntime.enqueue.mock.calls[0][2])).rejects.toMatchObject({
|
|
250
|
+
statusCode: 409,
|
|
251
|
+
message: change === 'moved' ? 'Scheduled target moved' : 'Scheduled task target changed or was removed',
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('cancels waiting prompts on disable, retarget and delete', async () => {
|
|
256
|
+
for (const input of [{ ...task, enabled: false }, { ...task, targetSessionId: 'ses_another' }]) {
|
|
257
|
+
const { runtime, projectConfigRuntime, messageQueueRuntime } = setup();
|
|
258
|
+
const service = createScheduledTaskService({
|
|
259
|
+
projectConfigRuntime, scheduledTasksRuntime: { validateTarget: runtime.validateTarget, cancelWaitingPrompt: runtime.cancel, syncProject: async () => [] },
|
|
260
|
+
readSettingsFromDiskMigrated: async () => ({ projects: [{ id: 'project-1', path: '/repo' }] }), sanitizeProjects: (projects) => projects,
|
|
261
|
+
});
|
|
262
|
+
await service.upsert('project-1', input);
|
|
263
|
+
expect(messageQueueRuntime.cancelScheduledTask).toHaveBeenCalledWith('project-1', task.id);
|
|
264
|
+
await service.remove('project-1', task.id);
|
|
265
|
+
expect(messageQueueRuntime.cancelScheduledTask).toHaveBeenCalledTimes(2);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it.each([404, 409])('allows pausing and editing an unchanged stale target with refusal %s', async (statusCode) => {
|
|
270
|
+
const { projectConfigRuntime, stored } = setup();
|
|
271
|
+
const validateTarget = vi.fn(async () => { throw Object.assign(new Error('stale target'), { statusCode }); });
|
|
272
|
+
const service = createScheduledTaskService({
|
|
273
|
+
projectConfigRuntime,
|
|
274
|
+
scheduledTasksRuntime: { validateTarget, cancelWaitingPrompt: async () => {}, syncProject: () => projectConfigRuntime.listScheduledTasks() },
|
|
275
|
+
readSettingsFromDiskMigrated: async () => ({ projects: [{ id: 'project-1', path: '/repo' }] }), sanitizeProjects: (projects) => projects,
|
|
276
|
+
});
|
|
277
|
+
expect(await service.setEnabled('project-1', task.id, false)).toMatchObject({ enabled: false });
|
|
278
|
+
expect((await service.upsert('project-1', { ...stored(), name: 'Paused edit' })).task.name).toBe('Paused edit');
|
|
279
|
+
expect(validateTarget).not.toHaveBeenCalled();
|
|
280
|
+
await expect(service.setEnabled('project-1', task.id, true)).rejects.toMatchObject({ statusCode });
|
|
281
|
+
await expect(service.upsert('project-1', { ...stored(), targetSessionId: 'ses_another' })).rejects.toMatchObject({ statusCode });
|
|
282
|
+
expect(validateTarget).toHaveBeenCalledTimes(2);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('keeps a waiting prompt when disabling or retargeting fails to save', async () => {
|
|
286
|
+
for (const input of [{ ...task, enabled: false }, { ...task, targetSessionId: 'ses_another' }]) {
|
|
287
|
+
const { runtime, projectConfigRuntime, messageQueueRuntime } = setup();
|
|
288
|
+
projectConfigRuntime.upsertScheduledTask = async () => { throw new Error('disk write failed'); };
|
|
289
|
+
const service = createScheduledTaskService({
|
|
290
|
+
projectConfigRuntime, scheduledTasksRuntime: { validateTarget: runtime.validateTarget, cancelWaitingPrompt: runtime.cancel, syncProject: async () => [] },
|
|
291
|
+
readSettingsFromDiskMigrated: async () => ({ projects: [{ id: 'project-1', path: '/repo' }] }), sanitizeProjects: (projects) => projects,
|
|
292
|
+
});
|
|
293
|
+
await expect(service.upsert('project-1', input)).rejects.toMatchObject({ statusCode: 500 });
|
|
294
|
+
expect(messageQueueRuntime.cancelScheduledTask).not.toHaveBeenCalled();
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('keeps a manually queued prompt when saving an already-paused task, but cancels on retarget', async () => {
|
|
299
|
+
const { runtime, projectConfigRuntime, messageQueueRuntime, stored } = setup();
|
|
300
|
+
await projectConfigRuntime.upsertScheduledTask('project-1', { ...task, enabled: false });
|
|
301
|
+
await runtime.run('project-1', stored(), 'manual');
|
|
302
|
+
const service = createScheduledTaskService({
|
|
303
|
+
projectConfigRuntime, scheduledTasksRuntime: { validateTarget: runtime.validateTarget, cancelWaitingPrompt: runtime.cancel, syncProject: () => projectConfigRuntime.listScheduledTasks() },
|
|
304
|
+
readSettingsFromDiskMigrated: async () => ({ projects: [{ id: 'project-1', path: '/repo' }] }), sanitizeProjects: (projects) => projects,
|
|
305
|
+
});
|
|
306
|
+
await service.upsert('project-1', { ...stored(), name: 'Paused edit' });
|
|
307
|
+
expect(stored().enabled).toBe(false);
|
|
308
|
+
expect(stored().state.lastStatus).toBe('queued');
|
|
309
|
+
expect(messageQueueRuntime.cancelScheduledTask).not.toHaveBeenCalled();
|
|
310
|
+
await service.upsert('project-1', { ...stored(), targetSessionId: 'ses_another' });
|
|
311
|
+
expect(messageQueueRuntime.cancelScheduledTask).toHaveBeenCalledWith('project-1', task.id);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it.each([
|
|
315
|
+
{ missing: false, claimed: true, consumeFails: false },
|
|
316
|
+
{ missing: true, claimed: true, consumeFails: false },
|
|
317
|
+
{ missing: false, claimed: false, consumeFails: false },
|
|
318
|
+
{ missing: false, claimed: true, consumeFails: true },
|
|
319
|
+
])('consumes only a claimed once attempt, missing=$missing claimed=$claimed consumeFails=$consumeFails', async ({ missing, claimed, consumeFails }) => {
|
|
320
|
+
vi.useFakeTimers();
|
|
321
|
+
vi.setSystemTime(new Date('2026-10-07T08:00:00Z'));
|
|
322
|
+
const { projectConfigRuntime, messageQueueRuntime, stored } = setup();
|
|
323
|
+
if (!claimed) projectConfigRuntime.updateScheduledTaskStateIf = async () => ({ task: stored(), updated: false });
|
|
324
|
+
await projectConfigRuntime.upsertScheduledTask('project-1', { ...task, schedule: { kind: 'once', date: '2026-10-07', time: '08:01', timezone: 'UTC' } });
|
|
325
|
+
if (consumeFails) projectConfigRuntime.upsertScheduledTask = async () => { throw new Error('disk write failed'); };
|
|
326
|
+
const warn = vi.fn();
|
|
327
|
+
vi.stubGlobal('fetch', async () => missing
|
|
328
|
+
? Response.json({ name: 'NotFoundError', message: 'Not found' }, { status: 404 })
|
|
329
|
+
: Response.json({ data: { id: 'ses_target', location: { directory: '/repo' } } }));
|
|
330
|
+
const scheduler = createScheduledTasksRuntime({
|
|
331
|
+
projectConfigRuntime, messageQueueRuntime, logger: { warn },
|
|
332
|
+
listProjects: async () => [{ id: 'project-1', path: '/repo' }],
|
|
333
|
+
chatsScope: { id: 'project-1', root: '/repo', contains: () => true },
|
|
334
|
+
buildOpenCodeUrl: (route) => `http://opencode.test${route}`, getOpenCodeAuthHeaders: () => ({}),
|
|
335
|
+
});
|
|
336
|
+
try {
|
|
337
|
+
await scheduler.start();
|
|
338
|
+
await vi.advanceTimersByTimeAsync(65_000);
|
|
339
|
+
expect(stored().enabled).toBe(!claimed || consumeFails);
|
|
340
|
+
expect(scheduler.getStatus().enabledScheduledTasksCount).toBe(claimed && !consumeFails ? 0 : 1);
|
|
341
|
+
expect(stored().state.lastStatus).toBe(claimed ? (missing ? 'failed' : 'queued') : undefined);
|
|
342
|
+
expect(messageQueueRuntime.enqueue).toHaveBeenCalledTimes(claimed && !missing ? 1 : 0);
|
|
343
|
+
expect(messageQueueRuntime.cancelScheduledTask).not.toHaveBeenCalled();
|
|
344
|
+
if (consumeFails) expect(warn).toHaveBeenCalledWith('[ScheduledTasks] failed to consume one-time task', { projectID: 'project-1', taskID: task.id, error: 'disk write failed' });
|
|
345
|
+
else expect(warn).not.toHaveBeenCalled();
|
|
346
|
+
} finally {
|
|
347
|
+
scheduler.stop();
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
});
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
* the markdown file; it continues to live in the project config/state store.
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
|
+
import { asNonEmptyString } from '../shared/guards.js';
|
|
40
41
|
import { createHash } from 'crypto';
|
|
41
42
|
import fs from 'fs';
|
|
42
43
|
import os from 'os';
|
|
@@ -47,14 +48,6 @@ import { MAX_TASK_NAME_LENGTH } from '../projects/project-config.js';
|
|
|
47
48
|
const LOOP_DIR_NAME = 'loops';
|
|
48
49
|
const USER_LOOP_ROOT = () => path.join(os.homedir(), '.agents', LOOP_DIR_NAME);
|
|
49
50
|
|
|
50
|
-
const asNonEmptyString = (value) => {
|
|
51
|
-
if (typeof value !== 'string') {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
const trimmed = value.trim();
|
|
55
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
51
|
/**
|
|
59
52
|
* Split a `provider/model` string into its two parts. Splits on the first `/`
|
|
60
53
|
* so model ids containing a slash (e.g. `openai/gpt-5`) still resolve.
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
+
import { asNonEmptyString } from '../shared/guards.js';
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
const asNonEmptyString = (value) => {
|
|
3
|
-
if (typeof value !== 'string') {
|
|
4
|
-
return null;
|
|
5
|
-
}
|
|
6
|
-
const trimmed = value.trim();
|
|
7
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
8
|
-
};
|
|
9
3
|
|
|
10
4
|
const parseProjectID = (req) => asNonEmptyString(req?.params?.projectId);
|
|
11
5
|
const parseTaskID = (req) => asNonEmptyString(req?.params?.taskId);
|
|
@@ -124,7 +118,13 @@ export const registerScheduledTaskRoutes = (app, dependencies) => {
|
|
|
124
118
|
try {
|
|
125
119
|
return res.json({ ok: true, ...await scheduledTaskService.run(projectID, taskID) });
|
|
126
120
|
} catch (error) {
|
|
127
|
-
if (error?.statusCode)
|
|
121
|
+
if (error?.statusCode) {
|
|
122
|
+
return res.status(error.statusCode).json({
|
|
123
|
+
error: error.message,
|
|
124
|
+
...(error.task ? { task: error.task } : {}),
|
|
125
|
+
...(error.busy ? { busy: error.busy } : {}),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
128
|
console.error('[ScheduledTasks] failed to run task:', error);
|
|
129
129
|
return res.status(500).json({ error: 'Failed to run scheduled task' });
|
|
130
130
|
}
|
|
@@ -5,6 +5,7 @@ import { expandSnippets } from '../opencode/snippets.js';
|
|
|
5
5
|
import { buildGoalIntroText, createSessionGoal } from '../session-goal/create.js';
|
|
6
6
|
import { discoverLoops } from './loops.js';
|
|
7
7
|
import { assertOpenCodeApiResponse } from '../opencode/prompt-response.js';
|
|
8
|
+
import { createExistingSessionTasks } from './existing-session.js';
|
|
8
9
|
|
|
9
10
|
const DEFAULT_GLOBAL_CONCURRENCY = 4;
|
|
10
11
|
const DEFAULT_PROJECT_CONCURRENCY = 2;
|
|
@@ -262,6 +263,12 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
262
263
|
// Chats scope (see chats-scope.js): scheduled like a project, but each run
|
|
263
264
|
// opens a new chat directory and loop files are not discovered for it.
|
|
264
265
|
chatsScope = null,
|
|
266
|
+
// The session defaults a task with `useDefaults` runs on:
|
|
267
|
+
// `(projectID) => { providerID, modelID, variant, agent }`, any of them null.
|
|
268
|
+
readSessionDefaults = null,
|
|
269
|
+
messageQueueRuntime,
|
|
270
|
+
resolvePrimaryWorktreeRoot,
|
|
271
|
+
isSessionArchived,
|
|
265
272
|
logger = console,
|
|
266
273
|
maxGlobalConcurrency = DEFAULT_GLOBAL_CONCURRENCY,
|
|
267
274
|
maxProjectConcurrency = DEFAULT_PROJECT_CONCURRENCY,
|
|
@@ -284,6 +291,37 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
284
291
|
},
|
|
285
292
|
});
|
|
286
293
|
|
|
294
|
+
const existingSessionTasks = messageQueueRuntime ? createExistingSessionTasks({
|
|
295
|
+
projectConfigRuntime, messageQueueRuntime, createClient: createScopedClient,
|
|
296
|
+
listProjects, chatsScope, resolvePrimaryWorktreeRoot, isSessionArchived, emitTaskRunEvent,
|
|
297
|
+
}) : null;
|
|
298
|
+
|
|
299
|
+
// A pinned task runs on what it stores; one that follows the session
|
|
300
|
+
// defaults reads them now, so changing a default reaches it on its next run.
|
|
301
|
+
const resolveRunSelection = async (projectID, task) => {
|
|
302
|
+
const execution = task.execution;
|
|
303
|
+
if (!execution.useDefaults) {
|
|
304
|
+
return {
|
|
305
|
+
providerID: execution.providerID ?? null,
|
|
306
|
+
modelID: execution.modelID ?? null,
|
|
307
|
+
variant: execution.variant ?? null,
|
|
308
|
+
agent: execution.agent ?? null,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
const defaults = typeof readSessionDefaults === 'function' ? await readSessionDefaults(projectID) : null;
|
|
313
|
+
return {
|
|
314
|
+
providerID: defaults?.providerID ?? null,
|
|
315
|
+
modelID: defaults?.modelID ?? null,
|
|
316
|
+
variant: defaults?.variant ?? null,
|
|
317
|
+
agent: defaults?.agent ?? null,
|
|
318
|
+
};
|
|
319
|
+
} catch (error) {
|
|
320
|
+
logger.warn?.('[scheduled-tasks] session defaults unavailable, using OpenCode defaults:', error?.message ?? error);
|
|
321
|
+
return { providerID: null, modelID: null, variant: null, agent: null };
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
287
325
|
let started = false;
|
|
288
326
|
const tasksByProject = new Map();
|
|
289
327
|
const projectPathByID = new Map();
|
|
@@ -574,7 +612,7 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
574
612
|
await recordKnowledge(sessionID, projectPath, knowledge);
|
|
575
613
|
};
|
|
576
614
|
|
|
577
|
-
const runTaskWithWatchdog = async (projectID, task, reason) => {
|
|
615
|
+
const runTaskWithWatchdog = async (projectID, task, reason, onSessionCreated) => {
|
|
578
616
|
const startedAt = Date.now();
|
|
579
617
|
const title = formatScheduledSessionTitle(task, startedAt);
|
|
580
618
|
const projectPath = projectPathByID.get(projectID);
|
|
@@ -595,6 +633,7 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
595
633
|
const baseUrl = openCodeOrigin();
|
|
596
634
|
const authHeaders = getOpenCodeAuthHeaders();
|
|
597
635
|
const client = createScopedClient(directory);
|
|
636
|
+
const selection = await resolveRunSelection(projectID, task);
|
|
598
637
|
|
|
599
638
|
// Agent, model and variant belong to the session in v2: a scheduled run
|
|
600
639
|
// fixes them here instead of repeating them on every prompt.
|
|
@@ -603,13 +642,15 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
603
642
|
const session = await client.session.create({
|
|
604
643
|
title,
|
|
605
644
|
location: { directory },
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
645
|
+
// No model or agent at all: OpenCode picks its own default.
|
|
646
|
+
...(selection.providerID && selection.modelID ? {
|
|
647
|
+
model: {
|
|
648
|
+
providerID: selection.providerID,
|
|
649
|
+
id: selection.modelID,
|
|
650
|
+
...(selection.variant ? { variant: selection.variant } : {}),
|
|
651
|
+
},
|
|
652
|
+
} : {}),
|
|
653
|
+
...(selection.agent ? { agent: selection.agent } : {}),
|
|
613
654
|
});
|
|
614
655
|
sessionID = session?.id;
|
|
615
656
|
if (!sessionID) {
|
|
@@ -622,6 +663,9 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
622
663
|
throw error;
|
|
623
664
|
}
|
|
624
665
|
|
|
666
|
+
// Before the event, so a UI refreshing on it already sees the session.
|
|
667
|
+
await onSessionCreated?.(sessionID);
|
|
668
|
+
|
|
625
669
|
try {
|
|
626
670
|
emitTaskRunEvent?.({
|
|
627
671
|
projectID,
|
|
@@ -658,8 +702,8 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
658
702
|
directory,
|
|
659
703
|
objective: commandObjective ?? expandSnippets(task.execution.prompt, directory),
|
|
660
704
|
tokenBudget: task.execution.goalTokenBudget,
|
|
661
|
-
providerID:
|
|
662
|
-
modelID:
|
|
705
|
+
providerID: selection.providerID ?? undefined,
|
|
706
|
+
modelID: selection.modelID ?? undefined,
|
|
663
707
|
onWarning: (message, error) => console.warn(`[scheduled-tasks] ${message}:`, error?.message || error),
|
|
664
708
|
});
|
|
665
709
|
}
|
|
@@ -733,6 +777,27 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
733
777
|
return { ok: false, skipped: true };
|
|
734
778
|
}
|
|
735
779
|
|
|
780
|
+
if (task.targetSessionId) {
|
|
781
|
+
if (!existingSessionTasks) return { ok: false, error: 'Existing-session scheduling is unavailable', statusCode: 503 };
|
|
782
|
+
const nextRunAt = computeNextRunAt(task, Math.max(Date.now(), (scheduledFor ?? 0) + 1));
|
|
783
|
+
const result = await existingSessionTasks.run(projectID, task, reason, scheduledFor, nextRunAt);
|
|
784
|
+
if (reason === 'scheduled') {
|
|
785
|
+
if (task.schedule.kind === 'once' && result.task && !result.skipped) {
|
|
786
|
+
try {
|
|
787
|
+
const consumed = await projectConfigRuntime.upsertScheduledTask(projectID, { ...result.task, enabled: false });
|
|
788
|
+
updateInMemoryTask(projectID, consumed.task);
|
|
789
|
+
} catch (consumeError) {
|
|
790
|
+
logger.warn?.('[ScheduledTasks] failed to consume one-time task', {
|
|
791
|
+
projectID,
|
|
792
|
+
taskID,
|
|
793
|
+
error: safeErrorMessage(consumeError),
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
} else scheduleFutureRun(projectID, taskID, nextRunAt);
|
|
797
|
+
}
|
|
798
|
+
return result;
|
|
799
|
+
}
|
|
800
|
+
|
|
736
801
|
const taskKey = buildTaskKey(projectID, taskID);
|
|
737
802
|
if (runningTaskKeys.has(taskKey)) {
|
|
738
803
|
return { ok: false, running: true };
|
|
@@ -762,6 +827,8 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
762
827
|
lastRunAt: runStartedAt,
|
|
763
828
|
lastStatus: 'running',
|
|
764
829
|
lastError: undefined,
|
|
830
|
+
// Set again once this run creates its session.
|
|
831
|
+
lastSessionId: undefined,
|
|
765
832
|
updatedAt: runStartedAt,
|
|
766
833
|
// Always set nextRunAt so a past once-slot is cleared when there is
|
|
767
834
|
// no following occurrence (omitting the key would leave the past value).
|
|
@@ -882,6 +949,7 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
882
949
|
lastRunAt: runStartedAt,
|
|
883
950
|
lastStatus: 'running',
|
|
884
951
|
lastError: undefined,
|
|
952
|
+
lastSessionId: undefined,
|
|
885
953
|
updatedAt: runStartedAt,
|
|
886
954
|
});
|
|
887
955
|
if (startResult.task) {
|
|
@@ -903,9 +971,27 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
903
971
|
let sessionDirectory;
|
|
904
972
|
let durationMs = 0;
|
|
905
973
|
let errorMessage;
|
|
974
|
+
// Known as soon as the run creates its session, so a run that fails or
|
|
975
|
+
// times out afterwards still points at the session it left behind.
|
|
976
|
+
let createdSessionID;
|
|
977
|
+
const recordCreatedSession = async (id) => {
|
|
978
|
+
createdSessionID = id;
|
|
979
|
+
try {
|
|
980
|
+
const recorded = await projectConfigRuntime.updateScheduledTaskState(projectID, taskID, { lastSessionId: id });
|
|
981
|
+
if (recorded.task) {
|
|
982
|
+
updateInMemoryTask(projectID, recorded.task);
|
|
983
|
+
}
|
|
984
|
+
} catch (error) {
|
|
985
|
+
logger.warn?.('[ScheduledTasks] failed to record the run session', {
|
|
986
|
+
projectID,
|
|
987
|
+
taskID,
|
|
988
|
+
error: safeErrorMessage(error),
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
};
|
|
906
992
|
|
|
907
993
|
try {
|
|
908
|
-
const runPromise = runTaskWithWatchdog(projectID, task, reason);
|
|
994
|
+
const runPromise = runTaskWithWatchdog(projectID, task, reason, recordCreatedSession);
|
|
909
995
|
let timeoutID;
|
|
910
996
|
const timeoutPromise = new Promise((_, reject) => {
|
|
911
997
|
timeoutID = setTimeout(() => {
|
|
@@ -967,7 +1053,7 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
967
1053
|
lastStatus: status,
|
|
968
1054
|
lastDurationMs: durationMs,
|
|
969
1055
|
lastError: status === 'error' ? errorMessage : undefined,
|
|
970
|
-
lastSessionId:
|
|
1056
|
+
lastSessionId: sessionID ?? createdSessionID,
|
|
971
1057
|
nextRunAt: Number.isFinite(nextRunAt) ? nextRunAt : undefined,
|
|
972
1058
|
updatedAt: finishedAt,
|
|
973
1059
|
};
|
|
@@ -1004,7 +1090,7 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
1004
1090
|
lastStatus: status,
|
|
1005
1091
|
lastDurationMs: durationMs,
|
|
1006
1092
|
lastError: status === 'error' ? errorMessage : undefined,
|
|
1007
|
-
lastSessionId:
|
|
1093
|
+
lastSessionId: sessionID ?? createdSessionID,
|
|
1008
1094
|
nextRunAt: Number.isFinite(nextRunAt) ? nextRunAt : undefined,
|
|
1009
1095
|
updatedAt: finishedAt,
|
|
1010
1096
|
},
|
|
@@ -1175,5 +1261,9 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
1175
1261
|
syncProject,
|
|
1176
1262
|
runNow,
|
|
1177
1263
|
getStatus,
|
|
1264
|
+
validateTarget: (projectID, sessionID) => existingSessionTasks.validateTarget(projectID, sessionID),
|
|
1265
|
+
beforeScheduledTaskSend: (sessionID, directory, item) => existingSessionTasks.beforeSend(sessionID, directory, item),
|
|
1266
|
+
onScheduledTaskResult: (provenance, status, error) => existingSessionTasks.result(provenance, status, error),
|
|
1267
|
+
cancelWaitingPrompt: (projectID, taskID) => existingSessionTasks?.cancel(projectID, taskID),
|
|
1178
1268
|
};
|
|
1179
1269
|
};
|