@thevinci/web 1.0.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 +197 -0
- package/bin/cli-entry.js +55 -0
- package/bin/cli-output.js +145 -0
- package/bin/cli.js +4887 -0
- package/bin/cli.test.js +64 -0
- package/dist/apple-touch-icon-120x120.png +0 -0
- package/dist/apple-touch-icon-152x152.png +0 -0
- package/dist/apple-touch-icon-167x167.png +0 -0
- package/dist/apple-touch-icon-180x180.png +0 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/apple-touch-icon.svg +528 -0
- package/dist/assets/JsonTreeView-CSm9OzXG.js +1 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/MarkdownRendererImpl-DensKOLc.js +6 -0
- package/dist/assets/MultiRunWindow-Bo7THayo.js +1 -0
- package/dist/assets/OnboardingScreen-BDqmzTVR.js +2 -0
- package/dist/assets/SettingsWindow-coz__Ykw.js +1 -0
- package/dist/assets/TerminalView-DrZ-i3Dr.js +1 -0
- package/dist/assets/ToolOutputDialog-Eglzslt3.js +16 -0
- package/dist/assets/es-4o9ciP61.js +15 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist/assets/index-DLTDToSP.css +1 -0
- package/dist/assets/index-DgiFEKGN.js +1 -0
- package/dist/assets/ko-B20imCHE.js +15 -0
- package/dist/assets/main-BV3KOtdA.css +1 -0
- package/dist/assets/main-CDKJj0sH.js +226 -0
- package/dist/assets/main-LC-PSNVM.js +2 -0
- package/dist/assets/miniChat-CQUiG_cr.js +2 -0
- package/dist/assets/modelPrefsAutoSave-Dm799vzR.js +6986 -0
- package/dist/assets/pl-DQJ7LSzj.js +15 -0
- package/dist/assets/pt-BR-OmjHUz9y.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-CARbeW0G.js +2 -0
- package/dist/assets/uk-BNFxOlO4.js +15 -0
- package/dist/assets/vendor--DBfsbEis.css +1 -0
- package/dist/assets/vendor-.bun-B9l0ZNi2.js +4094 -0
- package/dist/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/assets/wasmSttWorker-Dtlxac_K.js +1 -0
- package/dist/assets/wasmSttWorker-oo7Dm_jy.js +1806 -0
- package/dist/assets/worker-CbT6TVo7.js +155 -0
- package/dist/assets/zh-CN-C6T-Ac7F.js +15 -0
- package/dist/favicon-16.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +528 -0
- package/dist/index.html +607 -0
- package/dist/logo-dark-192x192.png +0 -0
- package/dist/logo-dark-512x512.png +0 -0
- package/dist/logo-dark-512x512.svg +528 -0
- package/dist/logo-light-192x192.png +0 -0
- package/dist/logo-light-512x512.png +0 -0
- package/dist/logo-light-512x512.svg +528 -0
- package/dist/mini-chat.html +16 -0
- package/dist/pwa-192.png +0 -0
- package/dist/pwa-512.png +0 -0
- package/dist/pwa-maskable-192.png +0 -0
- package/dist/pwa-maskable-512.png +0 -0
- package/dist/site.webmanifest +21 -0
- package/dist/sw.js +1 -0
- package/package.json +118 -0
- package/public/apple-touch-icon-120x120.png +0 -0
- package/public/apple-touch-icon-152x152.png +0 -0
- package/public/apple-touch-icon-167x167.png +0 -0
- package/public/apple-touch-icon-180x180.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/apple-touch-icon.svg +528 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +528 -0
- package/public/logo-dark-192x192.png +0 -0
- package/public/logo-dark-512x512.png +0 -0
- package/public/logo-dark-512x512.svg +528 -0
- package/public/logo-light-192x192.png +0 -0
- package/public/logo-light-512x512.png +0 -0
- package/public/logo-light-512x512.svg +528 -0
- package/public/pwa-192.png +0 -0
- package/public/pwa-512.png +0 -0
- package/public/pwa-maskable-192.png +0 -0
- package/public/pwa-maskable-512.png +0 -0
- package/public/site.webmanifest +21 -0
- package/server/TERMINAL_WS_PROTOCOL.md +48 -0
- package/server/index.d.ts +39 -0
- package/server/index.js +1311 -0
- package/server/lib/cloudflare-tunnel.js +650 -0
- package/server/lib/event-stream/DOCUMENTATION.md +61 -0
- package/server/lib/event-stream/directory-ws-bridge.js +185 -0
- package/server/lib/event-stream/global-hub.js +158 -0
- package/server/lib/event-stream/global-hub.test.js +140 -0
- package/server/lib/event-stream/global-ws-bridge.js +206 -0
- package/server/lib/event-stream/index.js +25 -0
- package/server/lib/event-stream/protocol.js +131 -0
- package/server/lib/event-stream/protocol.test.js +182 -0
- package/server/lib/event-stream/runtime.js +180 -0
- package/server/lib/event-stream/runtime.test.js +512 -0
- package/server/lib/event-stream/upstream-reader.js +226 -0
- package/server/lib/event-stream/upstream-reader.test.js +276 -0
- package/server/lib/fs/DOCUMENTATION.md +36 -0
- package/server/lib/fs/routes.js +1040 -0
- package/server/lib/fs/search.js +238 -0
- package/server/lib/git/DOCUMENTATION.md +152 -0
- package/server/lib/git/credentials.js +74 -0
- package/server/lib/git/identity-storage.js +112 -0
- package/server/lib/git/index.js +6 -0
- package/server/lib/git/routes.js +972 -0
- package/server/lib/git/service.js +3432 -0
- package/server/lib/git/service.test.js +39 -0
- package/server/lib/github/DOCUMENTATION.md +171 -0
- package/server/lib/github/auth.js +307 -0
- package/server/lib/github/device-flow.js +50 -0
- package/server/lib/github/index.js +24 -0
- package/server/lib/github/octokit.js +10 -0
- package/server/lib/github/pr-status.js +519 -0
- package/server/lib/github/repo/fork-detection.js +102 -0
- package/server/lib/github/repo/index.js +55 -0
- package/server/lib/github/routes.js +1560 -0
- package/server/lib/magic-prompts/routes.js +63 -0
- package/server/lib/magic-prompts/runtime.js +119 -0
- package/server/lib/notifications/DOCUMENTATION.md +122 -0
- package/server/lib/notifications/emitter-runtime.js +102 -0
- package/server/lib/notifications/index.js +4 -0
- package/server/lib/notifications/message.js +52 -0
- package/server/lib/notifications/message.test.js +34 -0
- package/server/lib/notifications/push-runtime.js +304 -0
- package/server/lib/notifications/routes.js +315 -0
- package/server/lib/notifications/runtime.js +566 -0
- package/server/lib/notifications/template-runtime.js +349 -0
- package/server/lib/notifications/template-runtime.test.js +26 -0
- package/server/lib/opencode/DOCUMENTATION.md +362 -0
- package/server/lib/opencode/agents.js +634 -0
- package/server/lib/opencode/auth-state-runtime.js +88 -0
- package/server/lib/opencode/auth.js +83 -0
- package/server/lib/opencode/bootstrap-runtime.js +131 -0
- package/server/lib/opencode/cli-entry-runtime.js +43 -0
- package/server/lib/opencode/cli-options.js +128 -0
- package/server/lib/opencode/commands.js +339 -0
- package/server/lib/opencode/config-entity-routes.js +370 -0
- package/server/lib/opencode/core-routes.js +500 -0
- package/server/lib/opencode/core-routes.test.js +26 -0
- package/server/lib/opencode/env-config.js +74 -0
- package/server/lib/opencode/env-keys.js +68 -0
- package/server/lib/opencode/env-runtime.js +1162 -0
- package/server/lib/opencode/env-runtime.test.js +116 -0
- package/server/lib/opencode/feature-routes-runtime.js +244 -0
- package/server/lib/opencode/hmr-state-runtime.js +85 -0
- package/server/lib/opencode/index.js +66 -0
- package/server/lib/opencode/lifecycle.js +1019 -0
- package/server/lib/opencode/lifecycle.test.js +240 -0
- package/server/lib/opencode/mcp.js +278 -0
- package/server/lib/opencode/network-runtime.js +104 -0
- package/server/lib/opencode/network-runtime.test.js +37 -0
- package/server/lib/opencode/opencode-resolution-runtime.js +71 -0
- package/server/lib/opencode/path-utils.js +100 -0
- package/server/lib/opencode/path-utils.test.js +71 -0
- package/server/lib/opencode/project-directory-runtime.js +124 -0
- package/server/lib/opencode/project-icon-routes.js +399 -0
- package/server/lib/opencode/project-icon-routes.test.js +107 -0
- package/server/lib/opencode/providers.js +96 -0
- package/server/lib/opencode/proxy.js +445 -0
- package/server/lib/opencode/pwa-manifest-routes.js +257 -0
- package/server/lib/opencode/pwa-manifest-routes.test.js +133 -0
- package/server/lib/opencode/routes.js +541 -0
- package/server/lib/opencode/server-startup-runtime.js +156 -0
- package/server/lib/opencode/server-utils-runtime.js +168 -0
- package/server/lib/opencode/server-utils-runtime.test.js +135 -0
- package/server/lib/opencode/session-runtime.js +356 -0
- package/server/lib/opencode/session-runtime.test.js +151 -0
- package/server/lib/opencode/settings-helpers.js +770 -0
- package/server/lib/opencode/settings-helpers.test.js +109 -0
- package/server/lib/opencode/settings-normalization-runtime.js +428 -0
- package/server/lib/opencode/settings-runtime.js +826 -0
- package/server/lib/opencode/settings-runtime.test.js +85 -0
- package/server/lib/opencode/shared.js +615 -0
- package/server/lib/opencode/shutdown-runtime.js +139 -0
- package/server/lib/opencode/shutdown-runtime.test.js +58 -0
- package/server/lib/opencode/skill-routes.js +701 -0
- package/server/lib/opencode/skills.js +548 -0
- package/server/lib/opencode/startup-pipeline-runtime.js +130 -0
- package/server/lib/opencode/static-routes-runtime.js +65 -0
- package/server/lib/opencode/theme-runtime.js +167 -0
- package/server/lib/opencode/tunnel-auth.js +591 -0
- package/server/lib/opencode/tunnel-wiring-runtime.js +94 -0
- package/server/lib/opencode/vinci-routes.js +76 -0
- package/server/lib/opencode/watcher.js +115 -0
- package/server/lib/opencode/watcher.test.js +239 -0
- package/server/lib/preview/proxy-runtime.js +1333 -0
- package/server/lib/preview/proxy-runtime.test.js +144 -0
- package/server/lib/projects/project-config.js +567 -0
- package/server/lib/projects/project-config.test.js +175 -0
- package/server/lib/projects/project-id.js +13 -0
- package/server/lib/quota/DOCUMENTATION.md +58 -0
- package/server/lib/quota/index.js +25 -0
- package/server/lib/quota/providers/claude.js +107 -0
- package/server/lib/quota/providers/codex.js +113 -0
- package/server/lib/quota/providers/copilot.js +165 -0
- package/server/lib/quota/providers/google/api.js +92 -0
- package/server/lib/quota/providers/google/auth.js +108 -0
- package/server/lib/quota/providers/google/index.js +124 -0
- package/server/lib/quota/providers/google/transforms.js +109 -0
- package/server/lib/quota/providers/index.js +168 -0
- package/server/lib/quota/providers/interface.js +55 -0
- package/server/lib/quota/providers/kimi.js +108 -0
- package/server/lib/quota/providers/minimax-cn-coding-plan.js +140 -0
- package/server/lib/quota/providers/minimax-coding-plan.js +139 -0
- package/server/lib/quota/providers/nanogpt.js +124 -0
- package/server/lib/quota/providers/ollama-cloud.js +112 -0
- package/server/lib/quota/providers/openai.js +91 -0
- package/server/lib/quota/providers/openrouter.js +92 -0
- package/server/lib/quota/providers/zai.js +91 -0
- package/server/lib/quota/providers/zhipuai-coding-plan.js +133 -0
- package/server/lib/quota/providers/zhipuai.js +114 -0
- package/server/lib/quota/routes.js +27 -0
- package/server/lib/quota/utils/auth.js +50 -0
- package/server/lib/quota/utils/formatters.js +85 -0
- package/server/lib/quota/utils/formatters.test.js +54 -0
- package/server/lib/quota/utils/index.js +10 -0
- package/server/lib/quota/utils/transformers.js +55 -0
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +44 -0
- package/server/lib/scheduled-tasks/routes.js +235 -0
- package/server/lib/scheduled-tasks/runtime.js +773 -0
- package/server/lib/scheduled-tasks/runtime.test.js +100 -0
- package/server/lib/security/request-security.js +115 -0
- package/server/lib/session-folders/routes.js +63 -0
- package/server/lib/session-folders/routes.test.js +102 -0
- package/server/lib/skills-catalog/DOCUMENTATION.md +178 -0
- package/server/lib/skills-catalog/cache.js +29 -0
- package/server/lib/skills-catalog/clawdhub/api.js +158 -0
- package/server/lib/skills-catalog/clawdhub/index.js +30 -0
- package/server/lib/skills-catalog/clawdhub/install.js +238 -0
- package/server/lib/skills-catalog/clawdhub/scan.js +113 -0
- package/server/lib/skills-catalog/curated-sources.js +21 -0
- package/server/lib/skills-catalog/git.js +77 -0
- package/server/lib/skills-catalog/index.js +42 -0
- package/server/lib/skills-catalog/install.js +294 -0
- package/server/lib/skills-catalog/scan.js +221 -0
- package/server/lib/skills-catalog/source.js +87 -0
- package/server/lib/terminal/DOCUMENTATION.md +76 -0
- package/server/lib/terminal/index.js +31 -0
- package/server/lib/terminal/output-replay-buffer.js +78 -0
- package/server/lib/terminal/output-replay-buffer.test.js +75 -0
- package/server/lib/terminal/runtime.js +850 -0
- package/server/lib/terminal/runtime.test.js +96 -0
- package/server/lib/terminal/terminal-ws-protocol.js +68 -0
- package/server/lib/terminal/terminal-ws-protocol.test.js +145 -0
- package/server/lib/text/DOCUMENTATION.md +35 -0
- package/server/lib/text/summarization.js +138 -0
- package/server/lib/text/summarization.test.js +34 -0
- package/server/lib/tts/DOCUMENTATION.md +146 -0
- package/server/lib/tts/base-url.js +62 -0
- package/server/lib/tts/capability-runtime.js +31 -0
- package/server/lib/tts/index.js +19 -0
- package/server/lib/tts/routes.js +261 -0
- package/server/lib/tts/routes.test.js +53 -0
- package/server/lib/tts/service.js +178 -0
- package/server/lib/tts/stt.js +75 -0
- package/server/lib/tunnels/DOCUMENTATION.md +18 -0
- package/server/lib/tunnels/index.js +166 -0
- package/server/lib/tunnels/managed-config.js +201 -0
- package/server/lib/tunnels/providers/cloudflare.js +260 -0
- package/server/lib/tunnels/registry.js +51 -0
- package/server/lib/tunnels/routes.js +605 -0
- package/server/lib/tunnels/types.js +219 -0
- package/server/lib/ui-auth/DOCUMENTATION.md +38 -0
- package/server/lib/ui-auth/ui-auth.js +673 -0
- package/server/lib/ui-auth/ui-passkeys.js +545 -0
- package/server/opencode-proxy.test.js +151 -0
- package/server/proxy-headers.js +61 -0
- package/server/proxy-headers.test.js +58 -0
- package/server/sse-routes.test.js +152 -0
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
import { createOpencodeClient } from '@opencode-ai/sdk/v2';
|
|
2
|
+
import { DateTime } from 'luxon';
|
|
3
|
+
import parser from 'cron-parser';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_GLOBAL_CONCURRENCY = 4;
|
|
6
|
+
const DEFAULT_PROJECT_CONCURRENCY = 2;
|
|
7
|
+
const DEFAULT_MAX_RUN_MS = 30 * 60 * 1000;
|
|
8
|
+
const JITTER_MAX_MS = 2_000;
|
|
9
|
+
const TASK_TITLE_MAX_LENGTH = 120;
|
|
10
|
+
const TASK_DUE_SLACK_MS = 5_000;
|
|
11
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
12
|
+
|
|
13
|
+
const buildTaskKey = (projectID, taskID) => `${projectID}:${taskID}`;
|
|
14
|
+
|
|
15
|
+
const parseTimeParts = (time) => {
|
|
16
|
+
const match = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(typeof time === 'string' ? time : '');
|
|
17
|
+
if (!match) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
hour: Number(match[1]),
|
|
22
|
+
minute: Number(match[2]),
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const applyTimeToDate = (baseDateTime, time) => {
|
|
27
|
+
const parsed = parseTimeParts(time);
|
|
28
|
+
if (!parsed) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return baseDateTime.set({
|
|
32
|
+
hour: parsed.hour,
|
|
33
|
+
minute: parsed.minute,
|
|
34
|
+
second: 0,
|
|
35
|
+
millisecond: 0,
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const resolveScheduleTimes = (schedule) => {
|
|
40
|
+
const times = [];
|
|
41
|
+
if (Array.isArray(schedule?.times)) {
|
|
42
|
+
for (const candidate of schedule.times) {
|
|
43
|
+
if (typeof candidate === 'string' && /^([01]\d|2[0-3]):([0-5]\d)$/.test(candidate)) {
|
|
44
|
+
times.push(candidate);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (times.length === 0 && typeof schedule?.time === 'string' && /^([01]\d|2[0-3]):([0-5]\d)$/.test(schedule.time)) {
|
|
49
|
+
times.push(schedule.time);
|
|
50
|
+
}
|
|
51
|
+
return Array.from(new Set(times)).sort((a, b) => a.localeCompare(b));
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const weekdayAsZeroBased = (dateTime) => {
|
|
55
|
+
if (!dateTime || typeof dateTime.weekday !== 'number') {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return dateTime.weekday % 7;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const safeErrorMessage = (error, maxLength = 2_000) => {
|
|
62
|
+
const raw = error instanceof Error
|
|
63
|
+
? (error.message || String(error))
|
|
64
|
+
: String(error ?? 'Unknown error');
|
|
65
|
+
const trimmed = raw.trim();
|
|
66
|
+
if (!trimmed) {
|
|
67
|
+
return 'Unknown error';
|
|
68
|
+
}
|
|
69
|
+
return trimmed.length > maxLength ? trimmed.slice(0, maxLength) : trimmed;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const parseScheduledCommandPrompt = (prompt) => {
|
|
73
|
+
if (typeof prompt !== 'string') {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const trimmed = prompt.trim();
|
|
78
|
+
if (!trimmed.startsWith('/')) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const firstLine = trimmed.split(/\r?\n/, 1)[0] || '';
|
|
83
|
+
const [head, ...tail] = firstLine.split(/\s+/);
|
|
84
|
+
const commandName = (head || '').slice(1).trim();
|
|
85
|
+
if (!commandName) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
command: commandName,
|
|
91
|
+
arguments: tail.join(' ').trim(),
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const computeNextRunAt = (task, nowMs = Date.now()) => {
|
|
96
|
+
if (!task?.enabled) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const schedule = task.schedule;
|
|
101
|
+
if (!schedule || typeof schedule !== 'object') {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const zone = typeof schedule.timezone === 'string' && schedule.timezone.trim().length > 0
|
|
106
|
+
? schedule.timezone.trim()
|
|
107
|
+
: DateTime.local().zoneName;
|
|
108
|
+
|
|
109
|
+
const now = DateTime.fromMillis(nowMs, { zone });
|
|
110
|
+
if (!now.isValid) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (schedule.kind === 'daily') {
|
|
115
|
+
const times = resolveScheduleTimes(schedule);
|
|
116
|
+
if (times.length === 0) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const minAllowed = now.plus({ milliseconds: TASK_DUE_SLACK_MS });
|
|
120
|
+
|
|
121
|
+
for (const time of times) {
|
|
122
|
+
const candidateToday = applyTimeToDate(now, time);
|
|
123
|
+
if (!candidateToday || !candidateToday.isValid) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (candidateToday > minAllowed) {
|
|
127
|
+
return candidateToday.toMillis();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const tomorrow = now.plus({ days: 1 });
|
|
132
|
+
const firstTomorrow = applyTimeToDate(tomorrow, times[0]);
|
|
133
|
+
return firstTomorrow?.isValid ? firstTomorrow.toMillis() : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (schedule.kind === 'weekly') {
|
|
137
|
+
if (!Array.isArray(schedule.weekdays) || schedule.weekdays.length === 0) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const times = resolveScheduleTimes(schedule);
|
|
141
|
+
if (times.length === 0) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const weekdaysSet = new Set(schedule.weekdays);
|
|
145
|
+
const minAllowed = now.plus({ milliseconds: TASK_DUE_SLACK_MS });
|
|
146
|
+
|
|
147
|
+
for (let dayOffset = 0; dayOffset <= 14; dayOffset += 1) {
|
|
148
|
+
const dayCandidate = now.plus({ days: dayOffset });
|
|
149
|
+
const zeroBasedWeekday = weekdayAsZeroBased(dayCandidate);
|
|
150
|
+
if (zeroBasedWeekday === null || !weekdaysSet.has(zeroBasedWeekday)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
for (const time of times) {
|
|
154
|
+
const withTime = applyTimeToDate(dayCandidate, time);
|
|
155
|
+
if (!withTime || !withTime.isValid) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (withTime > minAllowed) {
|
|
159
|
+
return withTime.toMillis();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (schedule.kind === 'once') {
|
|
167
|
+
if (typeof schedule.date !== 'string' || typeof schedule.time !== 'string') {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const parsed = DateTime.fromFormat(
|
|
172
|
+
`${schedule.date} ${schedule.time}`,
|
|
173
|
+
'yyyy-LL-dd HH:mm',
|
|
174
|
+
{ zone },
|
|
175
|
+
);
|
|
176
|
+
if (!parsed.isValid) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const minAllowed = now.plus({ milliseconds: TASK_DUE_SLACK_MS });
|
|
181
|
+
if (parsed <= minAllowed) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return parsed.toMillis();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (schedule.kind === 'cron') {
|
|
189
|
+
try {
|
|
190
|
+
const iterator = parser.parseExpression(schedule.cron, {
|
|
191
|
+
tz: zone,
|
|
192
|
+
currentDate: new Date(nowMs),
|
|
193
|
+
});
|
|
194
|
+
return iterator.next().getTime();
|
|
195
|
+
} catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return null;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export const formatScheduledSessionTitle = (task, nowMs = Date.now()) => {
|
|
204
|
+
const timezone = typeof task?.schedule?.timezone === 'string' && task.schedule.timezone.trim().length > 0
|
|
205
|
+
? task.schedule.timezone.trim()
|
|
206
|
+
: DateTime.local().zoneName;
|
|
207
|
+
const stamp = DateTime.fromMillis(nowMs, { zone: timezone }).toFormat('yyyy-LL-dd HH:mm');
|
|
208
|
+
const taskName = typeof task?.name === 'string' && task.name.trim().length > 0
|
|
209
|
+
? task.name.trim()
|
|
210
|
+
: 'Scheduled task';
|
|
211
|
+
const suffix = ` ${stamp}`;
|
|
212
|
+
const maxTaskNameLength = Math.max(1, TASK_TITLE_MAX_LENGTH - suffix.length);
|
|
213
|
+
const trimmedName = taskName.length > maxTaskNameLength
|
|
214
|
+
? taskName.slice(0, maxTaskNameLength)
|
|
215
|
+
: taskName;
|
|
216
|
+
return `${trimmedName}${suffix}`;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export const createScheduledTasksRuntime = (deps) => {
|
|
220
|
+
const {
|
|
221
|
+
projectConfigRuntime,
|
|
222
|
+
listProjects,
|
|
223
|
+
buildOpenCodeUrl,
|
|
224
|
+
getOpenCodeAuthHeaders,
|
|
225
|
+
waitForOpenCodeReady,
|
|
226
|
+
emitTaskRunEvent,
|
|
227
|
+
logger = console,
|
|
228
|
+
maxGlobalConcurrency = DEFAULT_GLOBAL_CONCURRENCY,
|
|
229
|
+
maxProjectConcurrency = DEFAULT_PROJECT_CONCURRENCY,
|
|
230
|
+
maxRunDurationMs = DEFAULT_MAX_RUN_MS,
|
|
231
|
+
} = deps;
|
|
232
|
+
|
|
233
|
+
let started = false;
|
|
234
|
+
const tasksByProject = new Map();
|
|
235
|
+
const projectPathByID = new Map();
|
|
236
|
+
const timersByTaskKey = new Map();
|
|
237
|
+
const queuedTaskKeys = new Set();
|
|
238
|
+
const runningTaskKeys = new Set();
|
|
239
|
+
const runningCountByProject = new Map();
|
|
240
|
+
let runningGlobalCount = 0;
|
|
241
|
+
const queue = [];
|
|
242
|
+
|
|
243
|
+
const clearTimerForKey = (taskKey) => {
|
|
244
|
+
const timer = timersByTaskKey.get(taskKey);
|
|
245
|
+
if (timer) {
|
|
246
|
+
clearTimeout(timer);
|
|
247
|
+
timersByTaskKey.delete(taskKey);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const clearProjectTimers = (projectID) => {
|
|
252
|
+
const tasks = tasksByProject.get(projectID);
|
|
253
|
+
if (!tasks) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
for (const task of tasks.values()) {
|
|
257
|
+
clearTimerForKey(buildTaskKey(projectID, task.id));
|
|
258
|
+
queuedTaskKeys.delete(buildTaskKey(projectID, task.id));
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const setProjectTasks = (projectID, tasks) => {
|
|
263
|
+
clearProjectTimers(projectID);
|
|
264
|
+
const taskMap = new Map();
|
|
265
|
+
for (const task of tasks) {
|
|
266
|
+
taskMap.set(task.id, task);
|
|
267
|
+
}
|
|
268
|
+
tasksByProject.set(projectID, taskMap);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const scheduleTask = (projectID, taskID, nextRunAt) => {
|
|
272
|
+
const taskKey = buildTaskKey(projectID, taskID);
|
|
273
|
+
clearTimerForKey(taskKey);
|
|
274
|
+
|
|
275
|
+
if (!started) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!Number.isFinite(nextRunAt) || nextRunAt <= 0) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const delayBase = Math.max(0, Math.round(nextRunAt - Date.now()));
|
|
284
|
+
const jitter = Math.floor(Math.random() * (JITTER_MAX_MS + 1));
|
|
285
|
+
const delay = delayBase + jitter;
|
|
286
|
+
const boundedDelay = Math.min(delay, MAX_TIMER_DELAY_MS);
|
|
287
|
+
|
|
288
|
+
const timer = setTimeout(async () => {
|
|
289
|
+
if (delay > MAX_TIMER_DELAY_MS) {
|
|
290
|
+
scheduleTask(projectID, taskID, nextRunAt);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
clearTimerForKey(taskKey);
|
|
295
|
+
const taskMap = tasksByProject.get(projectID);
|
|
296
|
+
const task = taskMap?.get(taskID);
|
|
297
|
+
if (!task || !task.enabled) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
queueTaskRun(projectID, taskID, 'scheduled');
|
|
301
|
+
pumpQueue();
|
|
302
|
+
}, boundedDelay);
|
|
303
|
+
|
|
304
|
+
timersByTaskKey.set(taskKey, timer);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const updateInMemoryTask = (projectID, nextTask) => {
|
|
308
|
+
if (!nextTask) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const taskMap = tasksByProject.get(projectID);
|
|
312
|
+
if (!taskMap) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
taskMap.set(nextTask.id, nextTask);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const syncTaskSchedule = async (projectID, task) => {
|
|
319
|
+
if (!task) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const nextRunAt = computeNextRunAt(task, Date.now());
|
|
323
|
+
const statePatch = {
|
|
324
|
+
nextRunAt: Number.isFinite(nextRunAt) ? nextRunAt : undefined,
|
|
325
|
+
updatedAt: Date.now(),
|
|
326
|
+
};
|
|
327
|
+
const result = await projectConfigRuntime.updateScheduledTaskState(projectID, task.id, statePatch);
|
|
328
|
+
if (result.task) {
|
|
329
|
+
updateInMemoryTask(projectID, result.task);
|
|
330
|
+
if (result.task.enabled && Number.isFinite(result.task.state?.nextRunAt)) {
|
|
331
|
+
scheduleTask(projectID, result.task.id, result.task.state.nextRunAt);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const ensureProjectPath = async (projectID) => {
|
|
337
|
+
if (projectPathByID.has(projectID)) {
|
|
338
|
+
return projectPathByID.get(projectID) || null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
const projects = await listProjects();
|
|
343
|
+
const project = projects.find((item) => item?.id === projectID && item?.path);
|
|
344
|
+
if (project?.path) {
|
|
345
|
+
projectPathByID.set(projectID, project.path);
|
|
346
|
+
return project.path;
|
|
347
|
+
}
|
|
348
|
+
} catch {
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return null;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const syncProject = async (projectID) => {
|
|
355
|
+
await ensureProjectPath(projectID);
|
|
356
|
+
|
|
357
|
+
const tasks = await projectConfigRuntime.listScheduledTasks(projectID);
|
|
358
|
+
setProjectTasks(projectID, tasks);
|
|
359
|
+
|
|
360
|
+
for (const task of tasks) {
|
|
361
|
+
await syncTaskSchedule(projectID, task);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return tasks;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const syncAllProjects = async () => {
|
|
368
|
+
const projects = await listProjects();
|
|
369
|
+
const activeProjectIDs = new Set();
|
|
370
|
+
projectPathByID.clear();
|
|
371
|
+
for (const project of projects) {
|
|
372
|
+
if (!project?.id || !project?.path) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
activeProjectIDs.add(project.id);
|
|
376
|
+
projectPathByID.set(project.id, project.path);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
for (const existingProjectID of Array.from(tasksByProject.keys())) {
|
|
380
|
+
if (!activeProjectIDs.has(existingProjectID)) {
|
|
381
|
+
clearProjectTimers(existingProjectID);
|
|
382
|
+
tasksByProject.delete(existingProjectID);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
for (const projectID of activeProjectIDs) {
|
|
387
|
+
await syncProject(projectID);
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const queueTaskRun = (projectID, taskID, reason) => {
|
|
392
|
+
const taskKey = buildTaskKey(projectID, taskID);
|
|
393
|
+
if (queuedTaskKeys.has(taskKey) || runningTaskKeys.has(taskKey)) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
queuedTaskKeys.add(taskKey);
|
|
397
|
+
queue.push({ projectID, taskID, reason });
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const canRunTask = (projectID) => {
|
|
401
|
+
if (runningGlobalCount >= maxGlobalConcurrency) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
const projectRunning = runningCountByProject.get(projectID) || 0;
|
|
405
|
+
return projectRunning < maxProjectConcurrency;
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const buildPromptAsyncPayload = (task) => ({
|
|
409
|
+
model: {
|
|
410
|
+
providerID: task.execution.providerID,
|
|
411
|
+
modelID: task.execution.modelID,
|
|
412
|
+
},
|
|
413
|
+
...(task.execution.agent ? { agent: task.execution.agent } : {}),
|
|
414
|
+
...(task.execution.variant ? { variant: task.execution.variant } : {}),
|
|
415
|
+
parts: [
|
|
416
|
+
{
|
|
417
|
+
type: 'text',
|
|
418
|
+
text: task.execution.prompt,
|
|
419
|
+
},
|
|
420
|
+
],
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const runPromptAsync = async ({ baseUrl, authHeaders, sessionID, projectPath, task }) => {
|
|
424
|
+
const promptUrl = new URL(`${baseUrl}/session/${encodeURIComponent(sessionID)}/prompt_async`);
|
|
425
|
+
promptUrl.searchParams.set('directory', projectPath);
|
|
426
|
+
const response = await fetch(promptUrl.toString(), {
|
|
427
|
+
method: 'POST',
|
|
428
|
+
headers: {
|
|
429
|
+
...authHeaders,
|
|
430
|
+
'content-type': 'application/json',
|
|
431
|
+
accept: 'application/json',
|
|
432
|
+
},
|
|
433
|
+
body: JSON.stringify(buildPromptAsyncPayload(task)),
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
if (!response.ok) {
|
|
437
|
+
const body = await response.text().catch(() => '');
|
|
438
|
+
throw new Error(`prompt_async failed (${response.status})${body ? `: ${body}` : ''}`);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const runScheduledCommandIfApplicable = async ({ client, projectPath, sessionID, task }) => {
|
|
443
|
+
const parsed = parseScheduledCommandPrompt(task?.execution?.prompt);
|
|
444
|
+
if (!parsed) {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
let commands = [];
|
|
449
|
+
try {
|
|
450
|
+
const response = await client.command.list({ directory: projectPath });
|
|
451
|
+
commands = Array.isArray(response?.data) ? response.data : [];
|
|
452
|
+
} catch {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const hasMatchingCommand = commands.some((command) => command?.name === parsed.command);
|
|
457
|
+
if (!hasMatchingCommand) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
await client.session.command({
|
|
462
|
+
sessionID,
|
|
463
|
+
directory: projectPath,
|
|
464
|
+
command: parsed.command,
|
|
465
|
+
arguments: parsed.arguments,
|
|
466
|
+
...(task.execution.agent ? { agent: task.execution.agent } : {}),
|
|
467
|
+
model: `${task.execution.providerID}/${task.execution.modelID}`,
|
|
468
|
+
...(task.execution.variant ? { variant: task.execution.variant } : {}),
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
return true;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const runTaskWithWatchdog = async (projectID, task, reason) => {
|
|
475
|
+
const startedAt = Date.now();
|
|
476
|
+
const title = formatScheduledSessionTitle(task, startedAt);
|
|
477
|
+
const projectPath = projectPathByID.get(projectID);
|
|
478
|
+
if (!projectPath) {
|
|
479
|
+
throw new Error('project path is unavailable');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (typeof waitForOpenCodeReady === 'function') {
|
|
483
|
+
await waitForOpenCodeReady(10_000, 250);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
|
|
487
|
+
const authHeaders = getOpenCodeAuthHeaders();
|
|
488
|
+
const client = createOpencodeClient({
|
|
489
|
+
baseUrl,
|
|
490
|
+
headers: authHeaders,
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const sessionResponse = await client.session.create({
|
|
494
|
+
directory: projectPath,
|
|
495
|
+
title,
|
|
496
|
+
});
|
|
497
|
+
const sessionID = sessionResponse?.data?.id;
|
|
498
|
+
if (!sessionID) {
|
|
499
|
+
throw new Error('failed to create session');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
emitTaskRunEvent?.({
|
|
504
|
+
projectID,
|
|
505
|
+
taskID: task.id,
|
|
506
|
+
ranAt: startedAt,
|
|
507
|
+
status: 'running',
|
|
508
|
+
sessionID,
|
|
509
|
+
});
|
|
510
|
+
} catch {
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const executedAsCommand = await runScheduledCommandIfApplicable({
|
|
514
|
+
client,
|
|
515
|
+
projectPath,
|
|
516
|
+
sessionID,
|
|
517
|
+
task,
|
|
518
|
+
});
|
|
519
|
+
if (!executedAsCommand) {
|
|
520
|
+
await runPromptAsync({
|
|
521
|
+
baseUrl,
|
|
522
|
+
authHeaders,
|
|
523
|
+
sessionID,
|
|
524
|
+
projectPath,
|
|
525
|
+
task,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const finishedAt = Date.now();
|
|
530
|
+
return {
|
|
531
|
+
sessionID,
|
|
532
|
+
durationMs: Math.max(0, finishedAt - startedAt),
|
|
533
|
+
reason,
|
|
534
|
+
startedAt,
|
|
535
|
+
finishedAt,
|
|
536
|
+
};
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const runTask = async (projectID, taskID, reason) => {
|
|
540
|
+
const taskMap = tasksByProject.get(projectID);
|
|
541
|
+
const task = taskMap?.get(taskID);
|
|
542
|
+
if (!task || !task.enabled) {
|
|
543
|
+
return { ok: false, skipped: true };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const taskKey = buildTaskKey(projectID, taskID);
|
|
547
|
+
if (runningTaskKeys.has(taskKey)) {
|
|
548
|
+
return { ok: false, running: true };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
runningTaskKeys.add(taskKey);
|
|
552
|
+
runningGlobalCount += 1;
|
|
553
|
+
runningCountByProject.set(projectID, (runningCountByProject.get(projectID) || 0) + 1);
|
|
554
|
+
|
|
555
|
+
const runStartedAt = Date.now();
|
|
556
|
+
await projectConfigRuntime.updateScheduledTaskState(projectID, taskID, {
|
|
557
|
+
lastRunAt: runStartedAt,
|
|
558
|
+
lastStatus: 'running',
|
|
559
|
+
lastError: undefined,
|
|
560
|
+
updatedAt: runStartedAt,
|
|
561
|
+
}).then((result) => {
|
|
562
|
+
if (result.task) {
|
|
563
|
+
updateInMemoryTask(projectID, result.task);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
let status = 'success';
|
|
568
|
+
let sessionID;
|
|
569
|
+
let durationMs = 0;
|
|
570
|
+
let errorMessage;
|
|
571
|
+
|
|
572
|
+
try {
|
|
573
|
+
const runPromise = runTaskWithWatchdog(projectID, task, reason);
|
|
574
|
+
let timeoutID;
|
|
575
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
576
|
+
timeoutID = setTimeout(() => {
|
|
577
|
+
reject(new Error('scheduled task run timed out'));
|
|
578
|
+
}, maxRunDurationMs);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
const result = await Promise.race([runPromise, timeoutPromise]).finally(() => {
|
|
582
|
+
if (timeoutID) {
|
|
583
|
+
clearTimeout(timeoutID);
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
sessionID = result.sessionID;
|
|
587
|
+
durationMs = result.durationMs;
|
|
588
|
+
status = 'success';
|
|
589
|
+
logger.info?.(
|
|
590
|
+
'[ScheduledTasks] run completed',
|
|
591
|
+
{ projectID, taskID, status, reason, sessionID, durationMs }
|
|
592
|
+
);
|
|
593
|
+
} catch (error) {
|
|
594
|
+
status = 'error';
|
|
595
|
+
errorMessage = safeErrorMessage(error);
|
|
596
|
+
logger.warn?.('[ScheduledTasks] run failed', {
|
|
597
|
+
projectID,
|
|
598
|
+
taskID,
|
|
599
|
+
reason,
|
|
600
|
+
status,
|
|
601
|
+
error: errorMessage,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const finishedAt = Date.now();
|
|
606
|
+
if (!durationMs) {
|
|
607
|
+
durationMs = Math.max(0, finishedAt - runStartedAt);
|
|
608
|
+
}
|
|
609
|
+
let latestTask = (tasksByProject.get(projectID)?.get(taskID)) || task;
|
|
610
|
+
const shouldConsumeOneTimeTask = latestTask?.schedule?.kind === 'once' && reason === 'scheduled';
|
|
611
|
+
if (shouldConsumeOneTimeTask && latestTask?.enabled) {
|
|
612
|
+
try {
|
|
613
|
+
const consumed = await projectConfigRuntime.upsertScheduledTask(projectID, {
|
|
614
|
+
...latestTask,
|
|
615
|
+
enabled: false,
|
|
616
|
+
});
|
|
617
|
+
latestTask = consumed.task || latestTask;
|
|
618
|
+
updateInMemoryTask(projectID, latestTask);
|
|
619
|
+
} catch (consumeError) {
|
|
620
|
+
logger.warn?.('[ScheduledTasks] failed to consume one-time task', {
|
|
621
|
+
projectID,
|
|
622
|
+
taskID,
|
|
623
|
+
error: safeErrorMessage(consumeError),
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const nextRunAt = computeNextRunAt(latestTask, finishedAt);
|
|
629
|
+
|
|
630
|
+
const statePatch = {
|
|
631
|
+
lastStatus: status,
|
|
632
|
+
lastDurationMs: durationMs,
|
|
633
|
+
lastError: status === 'error' ? errorMessage : undefined,
|
|
634
|
+
lastSessionId: status === 'success' ? sessionID : undefined,
|
|
635
|
+
nextRunAt: Number.isFinite(nextRunAt) ? nextRunAt : undefined,
|
|
636
|
+
updatedAt: finishedAt,
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
const stateResult = await projectConfigRuntime.updateScheduledTaskState(projectID, taskID, statePatch);
|
|
640
|
+
if (stateResult.task) {
|
|
641
|
+
updateInMemoryTask(projectID, stateResult.task);
|
|
642
|
+
if (stateResult.task.enabled && Number.isFinite(stateResult.task.state?.nextRunAt)) {
|
|
643
|
+
scheduleTask(projectID, taskID, stateResult.task.state.nextRunAt);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
try {
|
|
648
|
+
emitTaskRunEvent?.({
|
|
649
|
+
projectID,
|
|
650
|
+
taskID,
|
|
651
|
+
ranAt: finishedAt,
|
|
652
|
+
status,
|
|
653
|
+
...(sessionID ? { sessionID } : {}),
|
|
654
|
+
});
|
|
655
|
+
} catch {
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
runningTaskKeys.delete(taskKey);
|
|
659
|
+
runningGlobalCount = Math.max(0, runningGlobalCount - 1);
|
|
660
|
+
const nextProjectCount = Math.max(0, (runningCountByProject.get(projectID) || 1) - 1);
|
|
661
|
+
if (nextProjectCount === 0) {
|
|
662
|
+
runningCountByProject.delete(projectID);
|
|
663
|
+
} else {
|
|
664
|
+
runningCountByProject.set(projectID, nextProjectCount);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return {
|
|
668
|
+
ok: status === 'success',
|
|
669
|
+
status,
|
|
670
|
+
sessionID,
|
|
671
|
+
task: stateResult.task || null,
|
|
672
|
+
error: errorMessage,
|
|
673
|
+
};
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
const pumpQueue = () => {
|
|
677
|
+
if (!started) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
let consumed = false;
|
|
682
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
683
|
+
const item = queue[index];
|
|
684
|
+
if (!canRunTask(item.projectID)) {
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
queue.splice(index, 1);
|
|
689
|
+
index -= 1;
|
|
690
|
+
|
|
691
|
+
const taskKey = buildTaskKey(item.projectID, item.taskID);
|
|
692
|
+
queuedTaskKeys.delete(taskKey);
|
|
693
|
+
consumed = true;
|
|
694
|
+
|
|
695
|
+
void runTask(item.projectID, item.taskID, item.reason).finally(() => {
|
|
696
|
+
pumpQueue();
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (!consumed && queue.length > 0) {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
const runNow = async (projectID, taskID) => {
|
|
706
|
+
const taskKey = buildTaskKey(projectID, taskID);
|
|
707
|
+
if (runningTaskKeys.has(taskKey)) {
|
|
708
|
+
return {
|
|
709
|
+
ok: false,
|
|
710
|
+
running: true,
|
|
711
|
+
error: 'task is already running',
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
if (queuedTaskKeys.has(taskKey)) {
|
|
715
|
+
return {
|
|
716
|
+
ok: false,
|
|
717
|
+
queued: true,
|
|
718
|
+
error: 'task is already queued',
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return runTask(projectID, taskID, 'manual');
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
const start = async () => {
|
|
726
|
+
if (started) {
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
started = true;
|
|
730
|
+
await syncAllProjects();
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
const stop = () => {
|
|
734
|
+
if (!started) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
started = false;
|
|
738
|
+
for (const timer of timersByTaskKey.values()) {
|
|
739
|
+
clearTimeout(timer);
|
|
740
|
+
}
|
|
741
|
+
timersByTaskKey.clear();
|
|
742
|
+
queuedTaskKeys.clear();
|
|
743
|
+
queue.length = 0;
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
const getStatus = () => {
|
|
747
|
+
let enabledCount = 0;
|
|
748
|
+
for (const taskMap of tasksByProject.values()) {
|
|
749
|
+
for (const task of taskMap.values()) {
|
|
750
|
+
if (task?.enabled) {
|
|
751
|
+
enabledCount += 1;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const runningCount = runningTaskKeys.size;
|
|
757
|
+
return {
|
|
758
|
+
hasEnabledScheduledTasks: enabledCount > 0,
|
|
759
|
+
hasRunningScheduledTasks: runningCount > 0,
|
|
760
|
+
enabledScheduledTasksCount: enabledCount,
|
|
761
|
+
runningScheduledTasksCount: runningCount,
|
|
762
|
+
};
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
return {
|
|
766
|
+
start,
|
|
767
|
+
stop,
|
|
768
|
+
syncAllProjects,
|
|
769
|
+
syncProject,
|
|
770
|
+
runNow,
|
|
771
|
+
getStatus,
|
|
772
|
+
};
|
|
773
|
+
};
|