@desplega.ai/agent-swarm 2.4.4 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/{acp-adapter-thq04adh.js → acp-adapter-11gp67nr.js} +8 -6
- package/dist/{actions-j6ywh6x5.js → actions-8p2kze8j.js} +24 -22
- package/dist/{amp-adapter-k8b5xmj8.js → amp-adapter-z4qvhacj.js} +2 -2
- package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
- package/dist/{app-rb7vc02s.js → app-0qp0a2gk.js} +3 -3
- package/dist/{assistant-ftta1zjf.js → assistant-jedd3ktm.js} +27 -25
- package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
- package/dist/{boot-reembed-xns7h5h9.js → boot-reembed-4xwq5tg0.js} +1 -1
- package/dist/{boot-reembed-srp6v6af.js → boot-reembed-n4wq5dd2.js} +1 -1
- package/dist/{boot-scrub-sweep-3qqrh2mz.js → boot-scrub-sweep-69h90xcq.js} +4 -4
- package/dist/{boot-seeds-9ks8hce7.js → boot-seeds-3vn0m9sg.js} +4 -4
- package/dist/{claude-adapter-k5ksyres.js → claude-adapter-221mvad5.js} +8 -6
- package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-gt6pa35d.js} +2 -2
- package/dist/{claude-managed-setup-n5a5eqks.js → claude-managed-setup-c3hp36ph.js} +4 -4
- package/dist/{claude-sdk-session-2j3j4kwn.js → claude-sdk-session-8thjsk5e.js} +9729 -9628
- package/dist/{cli-b9fcxxev.js → cli-007cnhm1.js} +50 -4
- package/dist/{cli-j4rqdf2n.js → cli-03vpq45j.js} +2 -2
- package/dist/{cli-amyhm3gj.js → cli-0y6f0ckq.js} +1 -1
- package/dist/{cli-4dg55wsk.js → cli-0z3vjrpm.js} +1 -1
- package/dist/{cli-pccfmtb5.js → cli-179xcyky.js} +11 -11
- package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
- package/dist/{cli-hj3debgw.js → cli-1pr672hz.js} +3 -3
- package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
- package/dist/{cli-t7cyef5g.js → cli-36j4jnk3.js} +4 -4
- package/dist/{cli-er83cjbn.js → cli-3qv8naq8.js} +527 -231
- package/dist/cli-3zwye9sn.js +1193 -0
- package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
- package/dist/{cli-xj0re5tm.js → cli-4w24f7z3.js} +1 -1
- package/dist/{cli-v3yr2gbg.js → cli-5kzk3472.js} +2198 -968
- package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
- package/dist/{cli-w6nadkh4.js → cli-5rvg1ddk.js} +5 -5
- package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
- package/dist/{cli-9qtjfd60.js → cli-6rg2jn2y.js} +3 -2
- package/dist/{cli-2v045xyf.js → cli-7bwtc6yx.js} +3 -3
- package/dist/{cli-3cgngyd8.js → cli-7g40g8hq.js} +35 -17
- package/dist/{cli-q6r7t299.js → cli-92mqenkc.js} +2379 -154
- package/dist/{cli-cynvc9fj.js → cli-9ce2xw98.js} +8 -8
- package/dist/{cli-gqvh8d8a.js → cli-bgdtv62r.js} +36 -22
- package/dist/{cli-wxbn3yfx.js → cli-cz5nmdxt.js} +2 -2
- package/dist/{cli-wt645v7q.js → cli-dabehxhp.js} +1 -1
- package/dist/{cli-vfha21c9.js → cli-e5bxnjps.js} +2 -2
- package/dist/{cli-p20r8w42.js → cli-fcc5qe3f.js} +763 -268
- package/dist/{cli-1ephgwnq.js → cli-fes51ya2.js} +3 -3
- package/dist/{cli-1d70crsy.js → cli-ftmq4nvk.js} +2 -2
- package/dist/{cli-qkhc5aqy.js → cli-fy1jyyym.js} +13 -13
- package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
- package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
- package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
- package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
- package/dist/{cli-bnz0r3kc.js → cli-j1eghqfm.js} +5 -5
- package/dist/{cli-2zqpkvtt.js → cli-jpxsrxcb.js} +1 -1
- package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
- package/dist/{cli-37yb7307.js → cli-mbfj11fq.js} +1 -1
- package/dist/{cli-pnp2j5zg.js → cli-mmhj8vd7.js} +2 -2
- package/dist/{cli-xf234ws8.js → cli-n4c8e3s5.js} +2 -0
- package/dist/{cli-a0kcty51.js → cli-n6kmqngw.js} +2 -2
- package/dist/{cli-te6b5wvg.js → cli-nnvjb17s.js} +3 -3
- package/dist/{cli-1dmcq2eh.js → cli-nzfnt2gs.js} +1 -1
- package/dist/{cli-625tb7aq.js → cli-p859hcc8.js} +2 -2
- package/dist/{cli-smvcw9z5.js → cli-pasyvjmm.js} +1 -1
- package/dist/cli-pkaqk57w.js +128 -0
- package/dist/{cli-8nyhf7za.js → cli-qqzmjxkf.js} +129 -105
- package/dist/{cli-zrwpctp4.js → cli-r25qpaxt.js} +161 -63
- package/dist/{cli-nc35pxb7.js → cli-rh5smt8t.js} +19 -4
- package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
- package/dist/{cli-esxerzg7.js → cli-s4sntwt4.js} +2 -2
- package/dist/{cli-b8k7epj1.js → cli-segs7hx9.js} +8 -8
- package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
- package/dist/{cli-vdm20wej.js → cli-sy8f9705.js} +4 -4
- package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
- package/dist/{cli-2y91wnbh.js → cli-we0ra057.js} +222 -10
- package/dist/{cli-55qmbp6z.js → cli-x21dqn18.js} +5 -5
- package/dist/{cli-dqb4n629.js → cli-x6vf4sad.js} +11 -11
- package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
- package/dist/{cli-vq97g1jq.js → cli-xawhpgc9.js} +95 -195
- package/dist/{cli-82d3nkc3.js → cli-xd4eqtr2.js} +2 -2
- package/dist/{cli-9fhkkpj5.js → cli-xt768r5m.js} +2 -2
- package/dist/{cli-zrk6jxv8.js → cli-y4x8ww6s.js} +2 -2
- package/dist/{cli-xc27fwzg.js → cli-y9jvxfhk.js} +3 -3
- package/dist/{cli-44159k28.js → cli-z63a269p.js} +2 -2
- package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
- package/dist/cli.js +31 -29
- package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
- package/dist/{codex-adapter-968xwfy7.js → codex-adapter-tcg1q3jk.js} +6 -4
- package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
- package/dist/{codex-session-runner-ht4z99fa.js → codex-session-runner-xfx4wd9x.js} +7 -5
- package/dist/{commands-pcymaeg9.js → commands-ew83nj70.js} +2 -2
- package/dist/{controller-fpjyk46r.js → controller-gnq1k1mf.js} +4 -4
- package/dist/{controllers-connect-mwzx1fh6.js → controllers-connect-tw1dan9r.js} +26 -24
- package/dist/{cursor-adapter-cp4s2e2f.js → cursor-adapter-yq5xgfcg.js} +1 -1
- package/dist/{db-xsjg9jqq.js → db-7gbc53fd.js} +14 -2
- package/dist/{db-sgd12bt9.js → db-t86xrnes.js} +3 -3
- package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
- package/dist/{dispatcher-x0jfw2c9.js → dispatcher-64bdyhps.js} +23 -21
- package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
- package/dist/{dsh-adapter-jrq7ma7h.js → dsh-adapter-tvhc4k2d.js} +1 -1
- package/dist/{e2b-16d6ch39.js → e2b-9gkdhm3c.js} +2 -2
- package/dist/{e2b-zn0b4w5w.js → e2b-cjtf8k1z.js} +2 -2
- package/dist/{easter-egg-3d-5cgynaja.js → easter-egg-3d-e4qv8dxj.js} +1 -1
- package/dist/{event-bus-9vpk8gmq.js → event-bus-3z78ep5n.js} +1 -1
- package/dist/{execute-t4e6m3rw.js → execute-0xpz8950.js} +85 -23
- package/dist/{external-channel-8njj4h3b.js → external-channel-e9nw24zv.js} +2 -2
- package/dist/{google-generative-ai-bmq8gkf3.js → google-generative-ai-mk9vcpe1.js} +15 -15
- package/dist/{google-vertex-b1rvp52s.js → google-vertex-q580kz8k.js} +20 -20
- package/dist/{grok-adapter-kkr97v2f.js → grok-adapter-v82srhrx.js} +9 -7
- package/dist/handlers-c9ajzhbn.js +41 -0
- package/dist/{heartbeat-wd03zhn0.js → heartbeat-2vqs1319.js} +26 -24
- package/dist/{hook-xzj2ts2p.js → hook-01nk13wx.js} +7 -5
- package/dist/{hook-f0vghj42.js → hook-jzpheegg.js} +9 -7
- package/dist/{http-4hwxv62p.js → http-fc1k6ec8.js} +859 -252
- package/dist/index-07ef7nkm.js +42 -0
- package/dist/{index-g6tbb6ve.js → index-31ceyf42.js} +1 -1
- package/dist/index-7yts8n9s.js +43 -0
- package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
- package/dist/index-q3t40ryv.js +46 -0
- package/dist/{index-ww2vznkp.js → index-sepja052.js} +1 -1
- package/dist/{index-97q4xn03.js → index-zdycv3ja.js} +135 -79
- package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
- package/dist/{keepalive-hrh7xnr9.js → keepalive-5nh2rhmr.js} +4 -4
- package/dist/lead-aayq64jn.js +79 -0
- package/dist/{llama-cpp-classify-vv43gswy.js → llama-cpp-classify-ee7vmd7q.js} +4 -1
- package/dist/{maintenance-e8wnwnqx.js → maintenance-hjhzdt3k.js} +1 -1
- package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
- package/dist/{node-3v3f08f7.js → node-jzjj7r9x.js} +10 -9
- package/dist/{oauth-refresh-sweep-zm0yj4sj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
- package/dist/{onboard-dtwcq603.js → onboard-1zq3m7db.js} +2 -2
- package/dist/{openai-codex-responses-de9pvs4d.js → openai-codex-responses-79s8xnbh.js} +29 -23
- package/dist/{openai-completions-mtqge3nf.js → openai-completions-tdbyts0x.js} +21 -20
- package/dist/{openai-decisions-ygk4kkxd.js → openai-decisions-bqhnjyfz.js} +2 -1
- package/dist/{openai-responses-dkyng71g.js → openai-responses-dm3t5vqp.js} +26 -17
- package/dist/{opencode-adapter-x4tqzhkh.js → opencode-adapter-e56086m1.js} +7 -5
- package/dist/{openrouter-images-wvv7camj.js → openrouter-images-ssttr1y4.js} +6 -3
- package/dist/{otel-impl-w3fbhpn0.js → otel-impl-h9yyza47.js} +2 -2
- package/dist/{pi-messages-nhh1pkz2.js → pi-messages-3nf8bq1a.js} +6 -4
- package/dist/{pi-mono-adapter-mcfkn9vf.js → pi-mono-adapter-tzps8f38.js} +9 -37
- package/dist/{pricing-refresh-j38dehfs.js → pricing-refresh-nevhat8h.js} +3 -3
- package/dist/{rbac-roles-ae8yttq4.js → rbac-roles-4y6ngvzq.js} +1 -1
- package/dist/{rbac-roles-ejxx9b35.js → rbac-roles-exbg8xb6.js} +2 -2
- package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
- package/dist/{render-v2-n16n0phe.js → render-v2-cfsh13z3.js} +3 -3
- package/dist/{resume-z0fgjc38.js → resume-5s2t0dbs.js} +10 -8
- package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
- package/dist/{session-access-cv3mq6mc.js → session-access-dnbyp3cn.js} +6 -4
- package/dist/{setup-mafhj3yt.js → setup-1wj1y77j.js} +1 -1
- package/dist/{typesafe-system-one-2hgpayzy.js → typesafe-system-one-bre38hvs.js} +4 -2
- package/dist/{virtual-modules-4r3zkc20.js → virtual-modules-mnfbh5mr.js} +8 -2
- package/dist/{work-objects-8b86mgjp.js → work-objects-7mg6216g.js} +26 -24
- package/dist/worker-nwb789mg.js +79 -0
- package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
- package/openapi.json +793 -6
- package/package.json +11 -11
- package/src/apps/catalog.generated.json +206 -4
- package/src/apps/page-validator.ts +13 -1
- package/src/assets/key.ts +17 -0
- package/src/azure-devops/handlers.ts +1 -1
- package/src/be/chunking.ts +42 -9
- package/src/be/db/approvals.ts +18 -0
- package/src/be/db/session-moves.ts +164 -0
- package/src/be/db/session-tree.ts +384 -0
- package/src/be/db/tasks/draft-text.ts +15 -0
- package/src/be/db/tasks/read.ts +54 -5
- package/src/be/db/workflow-runs.ts +96 -0
- package/src/be/db.ts +85 -237
- package/src/be/memory/index-content.ts +36 -17
- package/src/be/memory/providers/openai-embedding.ts +115 -61
- package/src/be/memory/providers/sqlite-store.ts +69 -23
- package/src/be/memory/reranker.ts +31 -1
- package/src/be/memory/search-candidates.ts +47 -0
- package/src/be/memory/types.ts +14 -0
- package/src/be/onboarding.ts +3 -0
- package/src/be/scripts/typecheck.ts +2 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +106 -54
- package/src/be/seed-scripts/index.ts +2 -2
- package/src/be/skill-sync.ts +43 -2
- package/src/be/swarm-config-guard.ts +25 -1
- package/src/be/task-attachment-store.ts +30 -0
- package/src/bitbucket/api.ts +81 -0
- package/src/bitbucket/auth.ts +64 -0
- package/src/bitbucket/handlers.ts +434 -0
- package/src/bitbucket/index.ts +28 -0
- package/src/bitbucket/templates.ts +134 -0
- package/src/bitbucket/types.ts +65 -0
- package/src/commands/runner.ts +55 -3
- package/src/e2b/env.ts +1 -0
- package/src/extensions/catalog.generated.json +4 -4
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +9 -2
- package/src/extensions/post-bridge.ts +1 -0
- package/src/fs/provider.ts +13 -1
- package/src/github/handlers.ts +6 -6
- package/src/gitlab/handlers.ts +5 -5
- package/src/heartbeat/heartbeat.ts +31 -11
- package/src/http/assets.ts +14 -2
- package/src/http/core.ts +12 -0
- package/src/http/fs.ts +54 -34
- package/src/http/index.ts +4 -0
- package/src/http/mcp-body.ts +37 -0
- package/src/http/mcp-user.ts +3 -5
- package/src/http/mcp.ts +6 -7
- package/src/http/memory.ts +26 -15
- package/src/http/onboarding.ts +1 -0
- package/src/http/page-proxy.ts +61 -5
- package/src/http/pages-public.ts +27 -44
- package/src/http/pages.ts +4 -4
- package/src/http/script-runs.ts +2 -0
- package/src/http/sessions.ts +366 -12
- package/src/http/skills.ts +48 -1
- package/src/http/tasks.ts +1 -1
- package/src/http/users.ts +13 -1
- package/src/http/webhooks.ts +141 -0
- package/src/integrations/chat-sdk/adapters.ts +24 -4
- package/src/integrations/chat-sdk/attachments.ts +90 -12
- package/src/integrations/chat-sdk/facts.ts +138 -0
- package/src/integrations/chat-sdk/inbound.ts +156 -8
- package/src/integrations/chat-sdk/limits.ts +24 -0
- package/src/integrations/chat-sdk/outbound-files.ts +186 -0
- package/src/integrations/chat-sdk/outbound.ts +272 -5
- package/src/integrations/chat-sdk/relay.ts +4 -1
- package/src/integrations/chat-sdk/router.ts +28 -2
- package/src/integrations/chat-sdk/steering.ts +9 -0
- package/src/integrations/chat-sdk/templates.ts +14 -0
- package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
- package/src/mcp-limits.ts +20 -0
- package/src/pages/browser-sdk.ts +8 -3
- package/src/pages/feedback-overlay.ts +1 -1
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +106 -119
- package/src/providers/codex-agents-md.ts +35 -229
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +5 -53
- package/src/realtime/transport.ts +1 -2
- package/src/script-workflows/workflow-ctx.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +2 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +14 -4
- package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
- package/src/server-user.ts +2 -0
- package/src/server.ts +2 -0
- package/src/slack/assistant.ts +2 -2
- package/src/slack/files.ts +2 -0
- package/src/slack/handlers.test.ts +158 -0
- package/src/slack/handlers.ts +23 -32
- package/src/slack/inbound-files.ts +39 -6
- package/src/slack/thread-buffer.ts +2 -1
- package/src/tasks/context-key.ts +61 -1
- package/src/tasks/task-terminal-effects.ts +21 -17
- package/src/tests/apps-card-grid.test.ts +197 -0
- package/src/tests/apps-catalog-additions.test.ts +120 -0
- package/src/tests/apps-tree.test.ts +129 -0
- package/src/tests/bitbucket-auth.test.ts +156 -0
- package/src/tests/bitbucket-handlers.test.ts +425 -0
- package/src/tests/bitbucket-webhook.test.ts +220 -0
- package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
- package/src/tests/chat-sdk-config.test.ts +11 -0
- package/src/tests/chat-sdk-facts.test.ts +23 -0
- package/src/tests/chat-sdk-inbound.test.ts +597 -4
- package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
- package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
- package/src/tests/codex-adapter.test.ts +167 -242
- package/src/tests/config-api-only-keys.test.ts +9 -0
- package/src/tests/context-key.test.ts +50 -0
- package/src/tests/core-auth.test.ts +5 -0
- package/src/tests/cors-allowlist.test.ts +1 -1
- package/src/tests/db-task-read-contract.test.ts +7 -0
- package/src/tests/db-task-write-contract.test.ts +6 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
- package/src/tests/fixtures/bitbucket/pullrequest-comment-created-inline.json +174 -0
- package/src/tests/fixtures/bitbucket/pullrequest-comment-created.json +170 -0
- package/src/tests/fixtures/bitbucket/pullrequest-created-mention.json +132 -0
- package/src/tests/fixtures/bitbucket/pullrequest-created-reviewer.json +140 -0
- package/src/tests/fixtures/bitbucket/pullrequest-fulfilled.json +134 -0
- package/src/tests/fixtures/bitbucket/pullrequest-rejected.json +133 -0
- package/src/tests/generate-mcp-docs.test.ts +101 -0
- package/src/tests/gitlab-handlers.test.ts +3 -3
- package/src/tests/gitlab-vcs-db.test.ts +4 -4
- package/src/tests/heartbeat-checklist.test.ts +33 -0
- package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
- package/src/tests/heartbeat-jev-gate.test.ts +242 -1
- package/src/tests/integration-disable-flags.test.ts +36 -0
- package/src/tests/kv-page-proxy.test.ts +91 -5
- package/src/tests/launch-password-rejection.test.ts +2 -2
- package/src/tests/mcp-input-caps.test.ts +294 -0
- package/src/tests/mcp-tools.test.ts +1 -1
- package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
- package/src/tests/memory-embedding-input-cap.test.ts +114 -0
- package/src/tests/memory.test.ts +14 -0
- package/src/tests/onboarding.test.ts +12 -0
- package/src/tests/page-guest-session.test.ts +19 -8
- package/src/tests/page-proxy-authed.test.ts +1 -1
- package/src/tests/page-proxy.test.ts +17 -10
- package/src/tests/page-session.test.ts +102 -1
- package/src/tests/pages-authed-mode.test.ts +82 -12
- package/src/tests/pages-password-mode.test.ts +5 -5
- package/src/tests/pi-codemode.test.ts +8 -2
- package/src/tests/preload-env.test.ts +57 -0
- package/src/tests/preload.ts +21 -0
- package/src/tests/prompt-template-remaining.test.ts +38 -0
- package/src/tests/realtime-transport.test.ts +13 -7
- package/src/tests/run-root-coverage.test.ts +39 -0
- package/src/tests/runner-bitbucket-vcs.test.ts +191 -0
- package/src/tests/runner-skills-refresh.test.ts +58 -0
- package/src/tests/script-runs-http.test.ts +84 -0
- package/src/tests/scripts-http.test.ts +10 -1
- package/src/tests/secret-scrubber.test.ts +15 -0
- package/src/tests/sessions-move.test.ts +576 -0
- package/src/tests/sessions-tree.test.ts +493 -0
- package/src/tests/skill-files-http.test.ts +50 -0
- package/src/tests/skills-bundle.test.ts +99 -0
- package/src/tests/swarm-config-export-to-env.test.ts +23 -3
- package/src/tests/task-audio-transcription.test.ts +299 -0
- package/src/tests/task-completion-memory-chunking.test.ts +434 -0
- package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
- package/src/tests/transcription.test.ts +299 -0
- package/src/tests/vcs-bitbucket.test.ts +91 -0
- package/src/tests/vcs-provider-isolation.test.ts +123 -0
- package/src/tests/vcs-provider.test.ts +16 -1
- package/src/tests/vcs-tracking.test.ts +3 -3
- package/src/tests/workflow-foreach-bodies.test.ts +637 -0
- package/src/tests/workflow-foreach-concurrency.test.ts +342 -0
- package/src/tests/workflow-foreach-retry.test.ts +537 -0
- package/src/tests/workflow-foreach.test.ts +44 -23
- package/src/tests/workflow-step-claims.test.ts +6 -6
- package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
- package/src/tests/workflow-system-one-decision.test.ts +2 -1
- package/src/tests/workflow-tla-races.test.ts +2 -2
- package/src/tools/chat-access.ts +10 -0
- package/src/tools/chat-post.ts +31 -11
- package/src/tools/chat-reply.ts +32 -10
- package/src/tools/memory-search.ts +3 -1
- package/src/tools/script-delete.ts +1 -1
- package/src/tools/system-one-decide.ts +17 -4
- package/src/tools/workflows/create-workflow.ts +5 -3
- package/src/types.ts +9 -2
- package/src/utils/config-env-export.ts +4 -1
- package/src/utils/page-session.ts +133 -20
- package/src/utils/secret-scrubber.ts +4 -0
- package/src/utils/skills-refresh.ts +83 -43
- package/src/utils/transcription.ts +246 -0
- package/src/vcs/bitbucket.ts +58 -0
- package/src/vcs/index.ts +2 -0
- package/src/vcs/types.ts +2 -2
- package/src/workflows/approval-resolution.ts +4 -1
- package/src/workflows/checkpoint.ts +9 -1
- package/src/workflows/constants.ts +3 -0
- package/src/workflows/definition.ts +48 -17
- package/src/workflows/engine.ts +25 -15
- package/src/workflows/executors/foreach.ts +45 -99
- package/src/workflows/executors/system-one-decision.ts +28 -7
- package/src/workflows/executors/system-one-providers.ts +39 -4
- package/src/workflows/foreach-body.ts +146 -0
- package/src/workflows/foreach-driver.ts +552 -0
- package/src/workflows/foreach-join.ts +206 -7
- package/src/workflows/node-timeout.ts +21 -0
- package/src/workflows/recovery.ts +68 -3
- package/src/workflows/resume.ts +221 -43
- package/src/workflows/retry-poller.ts +39 -24
- package/src/workflows/step-input.ts +59 -0
- package/src/workflows/task-step-routing.ts +114 -56
- package/templates/extensions/slack-reply-gate/README.md +24 -22
- package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
- package/templates/extensions/slack-reply-gate/manifest.json +2 -2
- package/templates/skills/apps/SKILL.md +28 -6
- package/templates/skills/apps/content.md +28 -6
- package/templates/skills/chat-sdk/content.md +6 -2
- package/templates/skills/code-quality/content.md +2 -1
- package/templates/skills/heartbeat-runbook/content.md +5 -5
- package/templates/skills/pages/content.md +6 -4
- package/templates/skills/slack-interaction/content.md +5 -0
- package/templates/skills/swarm-scripts/SKILL.md +2 -2
- package/templates/skills/swarm-scripts/content.md +2 -2
- package/templates/skills/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
- package/dist/cli-51y09c6p.js +0 -2657
- package/dist/cli-yw91rnmw.js +0 -13
- package/dist/handlers-0pvee9nw.js +0 -39
- package/dist/index-gdh8jrdm.js +0 -43
- package/dist/index-hg0x2cdv.js +0 -40
- package/dist/index-thm0y6a8.js +0 -41
- package/dist/lead-ee8vazz2.js +0 -77
- package/dist/worker-16s75hm5.js +0 -77
package/dist/cli-51y09c6p.js
DELETED
|
@@ -1,2657 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
telemetry,
|
|
3
|
-
init_telemetry,
|
|
4
|
-
scrubJsonValue,
|
|
5
|
-
init_scrub_json,
|
|
6
|
-
getDbClient2,
|
|
7
|
-
getTaskById2,
|
|
8
|
-
cancelTask2,
|
|
9
|
-
getChildWorkflowRunId,
|
|
10
|
-
getWorkflowRunLineage,
|
|
11
|
-
init_workflow_runs,
|
|
12
|
-
getOAuthApp,
|
|
13
|
-
getOAuthTokens,
|
|
14
|
-
init_oauth,
|
|
15
|
-
WorkflowReplayStateError,
|
|
16
|
-
init_workflow_replay,
|
|
17
|
-
getSwarmConfigs2,
|
|
18
|
-
getWorkflow2,
|
|
19
|
-
createWorkflowRun2,
|
|
20
|
-
getWorkflowRun2,
|
|
21
|
-
updateWorkflowRun2,
|
|
22
|
-
createWorkflowRunStep2,
|
|
23
|
-
getWorkflowRunStep2,
|
|
24
|
-
updateWorkflowRunStep2,
|
|
25
|
-
getWorkflowRunStepsByRunId2,
|
|
26
|
-
getLastSuccessfulRun2,
|
|
27
|
-
getLastRunStart2,
|
|
28
|
-
getCompletedStepNodeIds2,
|
|
29
|
-
getTaskByWorkflowRunStepId2,
|
|
30
|
-
detachTaskFromWorkflowRunStep2,
|
|
31
|
-
getStepByIdempotencyKey2,
|
|
32
|
-
getStepCountForNode2,
|
|
33
|
-
getLatestStepForNode2,
|
|
34
|
-
getCurrentStepForNode2,
|
|
35
|
-
cancelPendingApprovalRequestsForRun2,
|
|
36
|
-
listCancelledApprovalRequestsForRun2,
|
|
37
|
-
getWaitStateById2,
|
|
38
|
-
getPendingEventWaitNames2,
|
|
39
|
-
getPendingWaitsByEvent2,
|
|
40
|
-
resolveWaitState2,
|
|
41
|
-
init_db3
|
|
42
|
-
} from "./cli-p20r8w42.js";
|
|
43
|
-
import {
|
|
44
|
-
BaseExecutor,
|
|
45
|
-
postApprovalCancellationUpdates,
|
|
46
|
-
isSystemOneNodeType,
|
|
47
|
-
systemOneRetryViolations,
|
|
48
|
-
systemOneUnresolvedError,
|
|
49
|
-
getNextTargets,
|
|
50
|
-
findEntryNodes,
|
|
51
|
-
resolveValidationPort,
|
|
52
|
-
getSuccessors
|
|
53
|
-
} from "./cli-q6r7t299.js";
|
|
54
|
-
import {
|
|
55
|
-
workflowEventBus2,
|
|
56
|
-
init_event_bus
|
|
57
|
-
} from "./cli-wt645v7q.js";
|
|
58
|
-
import {
|
|
59
|
-
RetryPolicySchema,
|
|
60
|
-
SUPERSEDED_WORKFLOW_TASK_REASON,
|
|
61
|
-
init_types
|
|
62
|
-
} from "./cli-nc35pxb7.js";
|
|
63
|
-
import {
|
|
64
|
-
getSlackConfiguration,
|
|
65
|
-
isSlackConfigured,
|
|
66
|
-
init_config
|
|
67
|
-
} from "./cli-qeaq3j6t.js";
|
|
68
|
-
import {
|
|
69
|
-
deepInterpolate,
|
|
70
|
-
init_template
|
|
71
|
-
} from "./cli-tx00c0h6.js";
|
|
72
|
-
import {
|
|
73
|
-
init_zod
|
|
74
|
-
} from "./cli-4chz1hye.js";
|
|
75
|
-
import {
|
|
76
|
-
string2,
|
|
77
|
-
number2,
|
|
78
|
-
unknown,
|
|
79
|
-
object2,
|
|
80
|
-
union2,
|
|
81
|
-
discriminatedUnion,
|
|
82
|
-
record2,
|
|
83
|
-
_enum2,
|
|
84
|
-
literal2
|
|
85
|
-
} from "./cli-d9f7gzeq.js";
|
|
86
|
-
import {
|
|
87
|
-
getAppUrl,
|
|
88
|
-
init_constants2
|
|
89
|
-
} from "./cli-gf2850q7.js";
|
|
90
|
-
import {
|
|
91
|
-
isEnvFlagEnabled,
|
|
92
|
-
init_env_flag
|
|
93
|
-
} from "./cli-cebvbc9t.js";
|
|
94
|
-
import {
|
|
95
|
-
isSensitiveKey,
|
|
96
|
-
scrubSecrets,
|
|
97
|
-
init_secret_scrubber
|
|
98
|
-
} from "./cli-xf234ws8.js";
|
|
99
|
-
|
|
100
|
-
// src/automation-preflight-alert.ts
|
|
101
|
-
init_constants2();
|
|
102
|
-
init_secret_scrubber();
|
|
103
|
-
async function notifyAutomationPreflightFailure(preflight) {
|
|
104
|
-
const channel = process.env.SLACK_ALERTS_CHANNEL?.trim();
|
|
105
|
-
if (!channel) {
|
|
106
|
-
console.warn("[Automation Preflight] SLACK_ALERTS_CHANNEL not set; skipping alert");
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
try {
|
|
110
|
-
const { getSlackApp } = await import("./app-rb7vc02s.js");
|
|
111
|
-
const app = getSlackApp();
|
|
112
|
-
if (!app) {
|
|
113
|
-
console.warn("[Automation Preflight] Slack not available, cannot send notification");
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
await app.client.chat.postMessage({
|
|
117
|
-
channel,
|
|
118
|
-
text: scrubSecrets(`⚠️ Automation needs setup
|
|
119
|
-
` + `Automation: ${preflight.kind} "${preflight.name}"
|
|
120
|
-
Missing params: ${preflight.missing.params.join(", ") || "none"}
|
|
121
|
-
Missing integrations: ${preflight.missing.integrations.join(", ") || "none"}
|
|
122
|
-
Fire SKIPPED. It was not retried or queued.
|
|
123
|
-
Fix setup: ${getAppUrl()}${preflight.fixUrl}`),
|
|
124
|
-
unfurl_links: false,
|
|
125
|
-
unfurl_media: false
|
|
126
|
-
});
|
|
127
|
-
} catch (error) {
|
|
128
|
-
console.error("[Automation Preflight] Failed to send Slack notification:", scrubSecrets(error instanceof Error ? error.message : String(error)));
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// src/be/automation-preflight.ts
|
|
133
|
-
init_config();
|
|
134
|
-
init_env_flag();
|
|
135
|
-
init_secret_scrubber();
|
|
136
|
-
init_db3();
|
|
137
|
-
init_oauth();
|
|
138
|
-
init_scrub_json();
|
|
139
|
-
var NEEDS_SETUP_PREFIX = "needs_setup:";
|
|
140
|
-
function hasValue(value) {
|
|
141
|
-
if (value == null)
|
|
142
|
-
return false;
|
|
143
|
-
if (typeof value === "string")
|
|
144
|
-
return value.trim().length > 0;
|
|
145
|
-
if (Array.isArray(value))
|
|
146
|
-
return value.length > 0;
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
149
|
-
var GITHUB_REPOSITORY_SLUG = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9][A-Za-z0-9._-]{0,99}(?:\.git)?$/;
|
|
150
|
-
var SAFE_SHELL_DATA = /^[A-Za-z0-9._~%/:@ -]+$/;
|
|
151
|
-
var DNS_NAME = /^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$/;
|
|
152
|
-
var SAFE_EXTERNAL_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
153
|
-
var SAFE_GIT_REF = /^(?!-)(?!.*(?:\.\.|@\{|\/\/))[A-Za-z0-9._/-]+$/;
|
|
154
|
-
var SAFE_TAG_PATTERN = /^(?!-)(?!.*(?:\.\.|\/\/))[A-Za-z0-9._*?/-]+$/;
|
|
155
|
-
var SAFE_RELATIVE_PATH = /^(?![-/])(?!.*(?:^|\/)\.\.(?:\/|$))[A-Za-z0-9._/-]+$/;
|
|
156
|
-
var SAFE_REPORT_NAME = /^(?!\.{1,2}$)[A-Za-z0-9][A-Za-z0-9._ -]{0,127}$/;
|
|
157
|
-
function isSafeRepository(value) {
|
|
158
|
-
if (!SAFE_SHELL_DATA.test(value) || value.trim() !== value || value.includes(" "))
|
|
159
|
-
return false;
|
|
160
|
-
const slug = value.replace(/^https:\/\/github\.com\//, "").replace(/^git@github\.com:/, "").replace(/\/$/, "");
|
|
161
|
-
return GITHUB_REPOSITORY_SLUG.test(slug);
|
|
162
|
-
}
|
|
163
|
-
function isSafeGscProperty(value) {
|
|
164
|
-
if (!SAFE_SHELL_DATA.test(value) || value.trim() !== value)
|
|
165
|
-
return false;
|
|
166
|
-
return value.split(" ").every((property) => {
|
|
167
|
-
if (!property)
|
|
168
|
-
return false;
|
|
169
|
-
if (property.startsWith("sc-domain:"))
|
|
170
|
-
return DNS_NAME.test(property.slice(10));
|
|
171
|
-
if (DNS_NAME.test(property))
|
|
172
|
-
return true;
|
|
173
|
-
try {
|
|
174
|
-
const url = new URL(property);
|
|
175
|
-
return url.protocol === "https:" && !url.username && !url.password && !url.port && !url.search && !url.hash && DNS_NAME.test(url.hostname);
|
|
176
|
-
} catch {
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
var PARAM_VALIDATORS = {
|
|
182
|
-
REPO_URL: (value) => typeof value === "string" && isSafeRepository(value),
|
|
183
|
-
GSC_PROPERTY: (value) => typeof value === "string" && isSafeGscProperty(value),
|
|
184
|
-
BRANCH: (value) => typeof value === "string" && SAFE_GIT_REF.test(value),
|
|
185
|
-
SCOPE_PATH: (value) => typeof value === "string" && SAFE_RELATIVE_PATH.test(value),
|
|
186
|
-
REPORT_NAME: (value) => typeof value === "string" && value.trim() === value && SAFE_REPORT_NAME.test(value),
|
|
187
|
-
TAG_PATTERN: (value) => typeof value === "string" && SAFE_TAG_PATTERN.test(value),
|
|
188
|
-
AGENT_FS_ORG_ID: (value) => typeof value === "string" && SAFE_EXTERNAL_ID.test(value),
|
|
189
|
-
ORG_ID: (value) => typeof value === "string" && SAFE_EXTERNAL_ID.test(value)
|
|
190
|
-
};
|
|
191
|
-
function isUsableParam(key, value) {
|
|
192
|
-
if (!hasValue(value))
|
|
193
|
-
return false;
|
|
194
|
-
return PARAM_VALIDATORS[key]?.(value) ?? true;
|
|
195
|
-
}
|
|
196
|
-
function uniqueSorted(values) {
|
|
197
|
-
return [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
|
198
|
-
}
|
|
199
|
-
function automationUrl(input) {
|
|
200
|
-
return input.kind === "schedule" ? `/schedules/${input.id}` : `/workflows/${input.id}`;
|
|
201
|
-
}
|
|
202
|
-
var INTEGRATION_FIX_URL = {
|
|
203
|
-
slack: "/settings/integrations/slack",
|
|
204
|
-
github: "/settings/integrations/github",
|
|
205
|
-
linear: "/settings/integrations/linear",
|
|
206
|
-
jira: "/settings/integrations/jira",
|
|
207
|
-
agentmail: "/settings/integrations/agentmail",
|
|
208
|
-
gsc: "/settings/integrations/gsc",
|
|
209
|
-
agentfs: "/settings/integrations/agentfs"
|
|
210
|
-
};
|
|
211
|
-
var INTEGRATION_FIRST_REQUIREMENTS = new Set([
|
|
212
|
-
"gsc",
|
|
213
|
-
"linear",
|
|
214
|
-
"agentmail"
|
|
215
|
-
]);
|
|
216
|
-
function automationIntegrationFixUrl(id) {
|
|
217
|
-
return INTEGRATION_FIX_URL[id];
|
|
218
|
-
}
|
|
219
|
-
function isNeedsSetupFailure(reason) {
|
|
220
|
-
return reason?.startsWith(NEEDS_SETUP_PREFIX) ?? false;
|
|
221
|
-
}
|
|
222
|
-
function preflightAutomation(input, setup) {
|
|
223
|
-
const params = input.params ?? {};
|
|
224
|
-
const guardedParams = Object.keys(params).filter((key) => (key in PARAM_VALIDATORS));
|
|
225
|
-
const missingParams = uniqueSorted([...input.requiredParams ?? [], ...guardedParams].filter((key) => !isUsableParam(key, params[key])));
|
|
226
|
-
const missingIntegrations = uniqueSorted((input.requires ?? []).filter((id) => setup[id] === "unverified"));
|
|
227
|
-
const missing = { params: missingParams, integrations: missingIntegrations };
|
|
228
|
-
const baseUrl = automationUrl(input);
|
|
229
|
-
const fixes = [
|
|
230
|
-
...missingParams.map((key) => ({
|
|
231
|
-
type: "param",
|
|
232
|
-
key,
|
|
233
|
-
url: `${baseUrl}?param=${encodeURIComponent(key)}`
|
|
234
|
-
})),
|
|
235
|
-
...missingIntegrations.map((key) => ({
|
|
236
|
-
type: "integration",
|
|
237
|
-
key,
|
|
238
|
-
url: automationIntegrationFixUrl(key)
|
|
239
|
-
}))
|
|
240
|
-
];
|
|
241
|
-
if (missingParams.length === 0 && missingIntegrations.length === 0) {
|
|
242
|
-
return {
|
|
243
|
-
id: input.id,
|
|
244
|
-
name: input.name,
|
|
245
|
-
kind: input.kind,
|
|
246
|
-
state: "running",
|
|
247
|
-
missing,
|
|
248
|
-
fixes,
|
|
249
|
-
fixUrl: baseUrl
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
const firstParam = missingParams[0];
|
|
253
|
-
const firstIntegration = (input.requires ?? []).find((id) => missingIntegrations.includes(id));
|
|
254
|
-
const integrationFirst = (input.requires ?? []).some((id) => INTEGRATION_FIRST_REQUIREMENTS.has(id));
|
|
255
|
-
const fixUrl = integrationFirst && firstIntegration ? automationIntegrationFixUrl(firstIntegration) : firstParam ? `${baseUrl}?param=${encodeURIComponent(firstParam)}` : firstIntegration ? automationIntegrationFixUrl(firstIntegration) : baseUrl;
|
|
256
|
-
const failureReason = `${NEEDS_SETUP_PREFIX} params=[${missingParams.join(",")}] integrations=[${missingIntegrations.join(",")}]`;
|
|
257
|
-
return {
|
|
258
|
-
id: input.id,
|
|
259
|
-
name: input.name,
|
|
260
|
-
kind: input.kind,
|
|
261
|
-
state: "needs_setup",
|
|
262
|
-
missing,
|
|
263
|
-
fixes,
|
|
264
|
-
fixUrl,
|
|
265
|
-
failureReason
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
var EXACT_TOKEN = /^\{\{\s*([^}]+?)\s*\}\}$/;
|
|
269
|
-
var TOKEN = /\{\{\s*([^}]+?)\s*\}\}/g;
|
|
270
|
-
function renderAutomationTokens(value, params) {
|
|
271
|
-
function render(current) {
|
|
272
|
-
if (typeof current === "string") {
|
|
273
|
-
const exact = EXACT_TOKEN.exec(current);
|
|
274
|
-
if (exact?.[1] && Object.hasOwn(params, exact[1]))
|
|
275
|
-
return params[exact[1]];
|
|
276
|
-
return current.replace(TOKEN, (token, key) => {
|
|
277
|
-
if (!Object.hasOwn(params, key))
|
|
278
|
-
return token;
|
|
279
|
-
const replacement = params[key];
|
|
280
|
-
return typeof replacement === "object" ? JSON.stringify(replacement) : String(replacement);
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
if (Array.isArray(current))
|
|
284
|
-
return current.map(render);
|
|
285
|
-
if (current != null && typeof current === "object") {
|
|
286
|
-
return Object.fromEntries(Object.entries(current).map(([key, nested]) => [key, render(nested)]));
|
|
287
|
-
}
|
|
288
|
-
return current;
|
|
289
|
-
}
|
|
290
|
-
return render(value);
|
|
291
|
-
}
|
|
292
|
-
function present(value) {
|
|
293
|
-
return !!value?.trim();
|
|
294
|
-
}
|
|
295
|
-
async function getAutomationSetupStates() {
|
|
296
|
-
const [linearTokens, jiraTokens, jiraApp] = await Promise.all([
|
|
297
|
-
getOAuthTokens("linear"),
|
|
298
|
-
getOAuthTokens("jira"),
|
|
299
|
-
getOAuthApp("jira")
|
|
300
|
-
]);
|
|
301
|
-
let jiraCloudId = false;
|
|
302
|
-
try {
|
|
303
|
-
const metadata = jiraApp?.metadata ? JSON.parse(jiraApp.metadata) : null;
|
|
304
|
-
jiraCloudId = !!(metadata && typeof metadata === "object" && metadata.cloudId);
|
|
305
|
-
} catch {
|
|
306
|
-
jiraCloudId = false;
|
|
307
|
-
}
|
|
308
|
-
const agentFsKey = process.env.API_AGENT_FS_API_KEY ?? process.env.AGENT_FS_API_KEY;
|
|
309
|
-
const agentFsOrg = process.env.AGENT_FS_DEFAULT_ORG_ID ?? process.env.AGENT_FS_SHARED_ORG_ID;
|
|
310
|
-
const gscConfigured = present(process.env.GSC_SERVICE_ACCOUNT_BASE64) || present(process.env.GSC_SERVICE_ACCOUNT_JSON);
|
|
311
|
-
return {
|
|
312
|
-
slack: getSlackConfiguration().mode === "socket" && isSlackConfigured() ? "configured" : "unverified",
|
|
313
|
-
github: present(process.env.GITHUB_WEBHOOK_SECRET) && present(process.env.GITHUB_APP_ID) && present(process.env.GITHUB_APP_PRIVATE_KEY) ? "verified" : "unverified",
|
|
314
|
-
linear: linearTokens ? "verified" : "unverified",
|
|
315
|
-
jira: jiraTokens && jiraCloudId ? "verified" : "unverified",
|
|
316
|
-
gsc: gscConfigured ? "verified" : "unverified",
|
|
317
|
-
agentmail: !isEnvFlagEnabled("AGENTMAIL_DISABLE", false) && present(process.env.AGENTMAIL_API_KEY) ? "verified" : "unverified",
|
|
318
|
-
agentfs: present(process.env.AGENT_FS_API_URL) && present(agentFsKey) && present(agentFsOrg) && present(process.env.AGENT_FS_DEFAULT_DRIVE_ID) ? "verified" : "unverified"
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
function schedulePreflightInput(schedule) {
|
|
322
|
-
return { ...schedule, kind: "schedule" };
|
|
323
|
-
}
|
|
324
|
-
function workflowPreflightInput(workflow) {
|
|
325
|
-
return { ...workflow, kind: "workflow" };
|
|
326
|
-
}
|
|
327
|
-
async function listEnabledAutomationPreflightInputs() {
|
|
328
|
-
const rows = await getDbClient2().query(`SELECT id, name, 'schedule' AS kind, params, requiredParams, requires
|
|
329
|
-
FROM scheduled_tasks
|
|
330
|
-
WHERE enabled = 1
|
|
331
|
-
UNION ALL
|
|
332
|
-
SELECT id, name, 'workflow' AS kind, params, requiredParams, requires
|
|
333
|
-
FROM workflows
|
|
334
|
-
WHERE enabled = 1`);
|
|
335
|
-
return rows.map((row) => ({
|
|
336
|
-
id: row.id,
|
|
337
|
-
name: row.name,
|
|
338
|
-
kind: row.kind,
|
|
339
|
-
params: JSON.parse(row.params),
|
|
340
|
-
requiredParams: JSON.parse(row.requiredParams),
|
|
341
|
-
requires: JSON.parse(row.requires)
|
|
342
|
-
}));
|
|
343
|
-
}
|
|
344
|
-
function utcDayBounds(now) {
|
|
345
|
-
const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
346
|
-
return { start: start.toISOString(), end: new Date(start.getTime() + 86400000).toISOString() };
|
|
347
|
-
}
|
|
348
|
-
async function recordSchedulePreflightFailure(scheduleId, failureReason, now = new Date) {
|
|
349
|
-
const message = scrubSecrets(failureReason);
|
|
350
|
-
return await getDbClient2().transaction(async (tx) => {
|
|
351
|
-
const row = await tx.get("SELECT lastErrorAt, lastErrorMessage FROM scheduled_tasks WHERE id = ?", [scheduleId]);
|
|
352
|
-
if (!row)
|
|
353
|
-
return false;
|
|
354
|
-
if (row.lastErrorMessage === message && row.lastErrorAt?.slice(0, 10) === now.toISOString().slice(0, 10)) {
|
|
355
|
-
return false;
|
|
356
|
-
}
|
|
357
|
-
await tx.run(`UPDATE scheduled_tasks
|
|
358
|
-
SET lastErrorAt = ?, lastErrorMessage = ?, lastUpdatedAt = ?
|
|
359
|
-
WHERE id = ?`, [now.toISOString(), message, now.toISOString(), scheduleId]);
|
|
360
|
-
return true;
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
async function recordWorkflowPreflightFailure(input) {
|
|
364
|
-
const now = input.now ?? new Date;
|
|
365
|
-
const bounds = utcDayBounds(now);
|
|
366
|
-
return await getDbClient2().transaction(async (tx) => {
|
|
367
|
-
const existing = await tx.get(`SELECT id FROM workflow_runs
|
|
368
|
-
WHERE workflowId = ? AND status = 'failed'
|
|
369
|
-
AND error LIKE 'needs_setup:%'
|
|
370
|
-
AND startedAt >= ? AND startedAt < ?
|
|
371
|
-
ORDER BY startedAt ASC LIMIT 1`, [input.workflowId, bounds.start, bounds.end]);
|
|
372
|
-
if (existing)
|
|
373
|
-
return { runId: existing.id, recorded: false };
|
|
374
|
-
const runId = crypto.randomUUID();
|
|
375
|
-
const timestamp = now.toISOString();
|
|
376
|
-
await tx.run(`INSERT INTO workflow_runs
|
|
377
|
-
(id, workflowId, status, triggerType, triggerData, error, created_by,
|
|
378
|
-
startedAt, lastUpdatedAt, finishedAt)
|
|
379
|
-
VALUES (?, ?, 'failed', ?, ?, ?, ?, ?, ?, ?)`, [
|
|
380
|
-
runId,
|
|
381
|
-
input.workflowId,
|
|
382
|
-
input.triggerType,
|
|
383
|
-
input.triggerData === undefined ? null : scrubJsonValue(input.triggerData),
|
|
384
|
-
scrubSecrets(input.failureReason),
|
|
385
|
-
input.createdBy ?? null,
|
|
386
|
-
timestamp,
|
|
387
|
-
timestamp,
|
|
388
|
-
timestamp
|
|
389
|
-
]);
|
|
390
|
-
return { runId, recorded: true };
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// src/workflows/engine.ts
|
|
395
|
-
init_db3();
|
|
396
|
-
init_workflow_replay();
|
|
397
|
-
init_telemetry();
|
|
398
|
-
|
|
399
|
-
// src/workflows/checkpoint.ts
|
|
400
|
-
init_db3();
|
|
401
|
-
async function checkpointStep(runId, stepId, nodeId, result, ctx) {
|
|
402
|
-
await getDbClient2().transaction(async () => {
|
|
403
|
-
await updateWorkflowRunStep2(stepId, {
|
|
404
|
-
status: "completed",
|
|
405
|
-
output: result.output,
|
|
406
|
-
nextPort: result.nextPort || undefined,
|
|
407
|
-
finishedAt: new Date().toISOString()
|
|
408
|
-
});
|
|
409
|
-
ctx[nodeId] = result.output;
|
|
410
|
-
await updateWorkflowRun2(runId, {
|
|
411
|
-
context: ctx
|
|
412
|
-
});
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
async function checkpointStepFailure(runId, stepId, error, retryCount, retryPolicy, options) {
|
|
416
|
-
const now = new Date().toISOString();
|
|
417
|
-
if (retryPolicy && retryCount < retryPolicy.maxRetries) {
|
|
418
|
-
const delay = calculateBackoff(retryPolicy, retryCount);
|
|
419
|
-
const nextRetryAt = new Date(Date.now() + delay).toISOString();
|
|
420
|
-
await updateWorkflowRunStep2(stepId, {
|
|
421
|
-
status: "failed",
|
|
422
|
-
error,
|
|
423
|
-
retryCount: retryCount + 1,
|
|
424
|
-
maxRetries: retryPolicy.maxRetries,
|
|
425
|
-
nextRetryAt
|
|
426
|
-
});
|
|
427
|
-
return { shouldRetry: true };
|
|
428
|
-
}
|
|
429
|
-
await updateWorkflowRunStep2(stepId, {
|
|
430
|
-
status: "failed",
|
|
431
|
-
error,
|
|
432
|
-
finishedAt: now,
|
|
433
|
-
nextRetryAt: null
|
|
434
|
-
});
|
|
435
|
-
const markRunFailed = options?.markRunFailed ?? true;
|
|
436
|
-
if (markRunFailed) {
|
|
437
|
-
await getDbClient2().transaction(async () => {
|
|
438
|
-
const run = await getWorkflowRun2(runId);
|
|
439
|
-
if (run?.status !== "running" && run?.status !== "waiting")
|
|
440
|
-
return;
|
|
441
|
-
await updateWorkflowRun2(runId, {
|
|
442
|
-
status: "failed",
|
|
443
|
-
error: `Step failed: ${error}`,
|
|
444
|
-
finishedAt: now
|
|
445
|
-
});
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
return { shouldRetry: false };
|
|
449
|
-
}
|
|
450
|
-
async function checkpointStepWaiting(runId, stepId, ctx) {
|
|
451
|
-
return getDbClient2().transaction(async () => {
|
|
452
|
-
const step = await getDbClient2().get(`UPDATE workflow_run_steps
|
|
453
|
-
SET status = 'waiting'
|
|
454
|
-
WHERE id = ? AND runId = ? AND status = 'running'
|
|
455
|
-
AND EXISTS (
|
|
456
|
-
SELECT 1 FROM workflow_runs
|
|
457
|
-
WHERE id = ? AND status IN ('running', 'waiting')
|
|
458
|
-
)
|
|
459
|
-
RETURNING id`, [stepId, runId, runId]);
|
|
460
|
-
if (!step)
|
|
461
|
-
return false;
|
|
462
|
-
await updateWorkflowRun2(runId, { status: "waiting", context: ctx });
|
|
463
|
-
return true;
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
function calculateBackoff(policy, attempt) {
|
|
467
|
-
let delay;
|
|
468
|
-
switch (policy.strategy) {
|
|
469
|
-
case "exponential": {
|
|
470
|
-
const base = policy.baseDelayMs * 2 ** attempt;
|
|
471
|
-
delay = Math.random() * Math.min(base, policy.maxDelayMs);
|
|
472
|
-
break;
|
|
473
|
-
}
|
|
474
|
-
case "linear":
|
|
475
|
-
delay = policy.baseDelayMs * (attempt + 1);
|
|
476
|
-
break;
|
|
477
|
-
case "static":
|
|
478
|
-
delay = policy.baseDelayMs;
|
|
479
|
-
break;
|
|
480
|
-
default:
|
|
481
|
-
delay = policy.baseDelayMs;
|
|
482
|
-
}
|
|
483
|
-
return Math.min(delay, policy.maxDelayMs);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// src/workflows/completed-step-routing.ts
|
|
487
|
-
init_db3();
|
|
488
|
-
|
|
489
|
-
// src/workflows/foreach-join.ts
|
|
490
|
-
init_db3();
|
|
491
|
-
var FOREACH_TERMINAL_STEP_STATUSES = new Set([
|
|
492
|
-
"completed",
|
|
493
|
-
"failed",
|
|
494
|
-
"cancelled",
|
|
495
|
-
"skipped"
|
|
496
|
-
]);
|
|
497
|
-
function parseSyntheticNodeId(nodeId) {
|
|
498
|
-
const separatorIndex = nodeId.indexOf("#");
|
|
499
|
-
if (separatorIndex <= 0 || separatorIndex === nodeId.length - 1)
|
|
500
|
-
return null;
|
|
501
|
-
return {
|
|
502
|
-
parentNodeId: nodeId.slice(0, separatorIndex),
|
|
503
|
-
itemKey: nodeId.slice(separatorIndex + 1)
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
function resolveForeachParent(def, nodeId) {
|
|
507
|
-
const parsed = parseSyntheticNodeId(nodeId);
|
|
508
|
-
if (!parsed)
|
|
509
|
-
return null;
|
|
510
|
-
const parent = def.nodes.find((node) => node.id === parsed.parentNodeId);
|
|
511
|
-
return parent?.type === "foreach" ? parent : null;
|
|
512
|
-
}
|
|
513
|
-
async function joinForeach(def, runId, childStep, ctx) {
|
|
514
|
-
const parent = resolveForeachParent(def, childStep.nodeId);
|
|
515
|
-
if (!parent) {
|
|
516
|
-
throw new Error(`Step "${childStep.nodeId}" is not a foreach child`);
|
|
517
|
-
}
|
|
518
|
-
const children = (await getWorkflowRunStepsByRunId2(runId)).filter((step) => resolveForeachParent(def, step.nodeId)?.id === parent.id).sort((a, b) => childIndex(a) - childIndex(b));
|
|
519
|
-
if (children.some((step) => !FOREACH_TERMINAL_STEP_STATUSES.has(step.status))) {
|
|
520
|
-
return { joined: false, parentNodeId: parent.id, successors: [] };
|
|
521
|
-
}
|
|
522
|
-
const parentStep = await getLatestStepForNode2(runId, parent.id);
|
|
523
|
-
if (!parentStep) {
|
|
524
|
-
throw new Error(`Waiting foreach parent step "${parent.id}" was not found`);
|
|
525
|
-
}
|
|
526
|
-
if (parentStep.status === "completed") {
|
|
527
|
-
return { joined: false, parentNodeId: parent.id, successors: [] };
|
|
528
|
-
}
|
|
529
|
-
const aggregate = buildForeachAggregate(children);
|
|
530
|
-
await checkpointStep(runId, parentStep.id, parent.id, { output: aggregate }, ctx);
|
|
531
|
-
return {
|
|
532
|
-
joined: true,
|
|
533
|
-
parentNodeId: parent.id,
|
|
534
|
-
successors: getSuccessors(def, parent.id)
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
function buildForeachAggregate(children) {
|
|
538
|
-
const orderedChildren = [...children].sort((a, b) => childIndex(a) - childIndex(b));
|
|
539
|
-
const results = orderedChildren.map((step) => {
|
|
540
|
-
const parsed = parseSyntheticNodeId(step.nodeId);
|
|
541
|
-
const status = resultStatus(step);
|
|
542
|
-
return {
|
|
543
|
-
itemKey: parsed.itemKey,
|
|
544
|
-
status,
|
|
545
|
-
output: step.output
|
|
546
|
-
};
|
|
547
|
-
});
|
|
548
|
-
const failedCount = results.filter((result) => result.status !== "completed").length;
|
|
549
|
-
const aggregate = {
|
|
550
|
-
results,
|
|
551
|
-
okCount: results.length - failedCount,
|
|
552
|
-
failedCount
|
|
553
|
-
};
|
|
554
|
-
return aggregate;
|
|
555
|
-
}
|
|
556
|
-
function childIndex(step) {
|
|
557
|
-
if (typeof step.input !== "object" || step.input === null)
|
|
558
|
-
return Number.MAX_SAFE_INTEGER;
|
|
559
|
-
const index = step.input.index;
|
|
560
|
-
return typeof index === "number" ? index : Number.MAX_SAFE_INTEGER;
|
|
561
|
-
}
|
|
562
|
-
function resultStatus(step) {
|
|
563
|
-
if (step.status !== "completed")
|
|
564
|
-
return step.status;
|
|
565
|
-
return step.error ? "failed" : "completed";
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// src/workflows/completed-step-routing.ts
|
|
569
|
-
async function loadCompletedStepRouting(def, runId, completedNodeIds) {
|
|
570
|
-
const activeEdges = new Set;
|
|
571
|
-
const stepsByNodeId = new Map;
|
|
572
|
-
for (const nodeId of completedNodeIds) {
|
|
573
|
-
if (resolveForeachParent(def, nodeId))
|
|
574
|
-
continue;
|
|
575
|
-
const step = await getLatestStepForNode2(runId, nodeId);
|
|
576
|
-
if (step)
|
|
577
|
-
stepsByNodeId.set(nodeId, step);
|
|
578
|
-
const successors = step?.nextPort ? getSuccessors(def, nodeId, step.nextPort) : getSuccessors(def, nodeId);
|
|
579
|
-
for (const successor of successors) {
|
|
580
|
-
activeEdges.add(`${nodeId}→${successor.id}`);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
return { activeEdges, stepsByNodeId };
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// src/workflows/cooldown.ts
|
|
587
|
-
init_db3();
|
|
588
|
-
function cooldownToMs(cooldown) {
|
|
589
|
-
let ms = 0;
|
|
590
|
-
if (cooldown.hours)
|
|
591
|
-
ms += cooldown.hours * 60 * 60 * 1000;
|
|
592
|
-
if (cooldown.minutes)
|
|
593
|
-
ms += cooldown.minutes * 60 * 1000;
|
|
594
|
-
if (cooldown.seconds)
|
|
595
|
-
ms += cooldown.seconds * 1000;
|
|
596
|
-
return ms;
|
|
597
|
-
}
|
|
598
|
-
async function shouldSkipCooldown(workflowId, cooldown) {
|
|
599
|
-
const cooldownMs = cooldownToMs(cooldown);
|
|
600
|
-
const now = Date.now();
|
|
601
|
-
const lastSuccess = await getLastSuccessfulRun2(workflowId);
|
|
602
|
-
if (lastSuccess?.finishedAt) {
|
|
603
|
-
const lastFinished = new Date(lastSuccess.finishedAt).getTime();
|
|
604
|
-
if (now - lastFinished < cooldownMs)
|
|
605
|
-
return true;
|
|
606
|
-
}
|
|
607
|
-
const lastAttempt = await getLastRunStart2(workflowId);
|
|
608
|
-
if (lastAttempt?.startedAt) {
|
|
609
|
-
const lastStarted = new Date(lastAttempt.startedAt).getTime();
|
|
610
|
-
if (now - lastStarted < cooldownMs)
|
|
611
|
-
return true;
|
|
612
|
-
}
|
|
613
|
-
return false;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// src/workflows/input.ts
|
|
617
|
-
init_db3();
|
|
618
|
-
init_secret_scrubber();
|
|
619
|
-
async function resolveInputs(input) {
|
|
620
|
-
const resolved = {};
|
|
621
|
-
for (const [key, value] of Object.entries(input)) {
|
|
622
|
-
resolved[key] = await resolveInputValue(value);
|
|
623
|
-
}
|
|
624
|
-
return resolved;
|
|
625
|
-
}
|
|
626
|
-
async function resolveInputValue(value) {
|
|
627
|
-
const envMatch = /^\$\{(.+)\}$/.exec(value);
|
|
628
|
-
if (envMatch?.[1]) {
|
|
629
|
-
const envName = envMatch[1];
|
|
630
|
-
const envValue = process.env[envName];
|
|
631
|
-
if (envValue === undefined) {
|
|
632
|
-
throw new Error(`Environment variable "${envName}" is not set`);
|
|
633
|
-
}
|
|
634
|
-
return envValue;
|
|
635
|
-
}
|
|
636
|
-
if (value.startsWith("secret.")) {
|
|
637
|
-
const secretName = value.slice("secret.".length);
|
|
638
|
-
const configs = await getSwarmConfigs2({ scope: "global", key: secretName });
|
|
639
|
-
const secretConfig = configs.find((c) => c.isSecret);
|
|
640
|
-
if (!secretConfig) {
|
|
641
|
-
throw new Error(`Secret "${secretName}" not found in config store`);
|
|
642
|
-
}
|
|
643
|
-
return secretConfig.value;
|
|
644
|
-
}
|
|
645
|
-
return value;
|
|
646
|
-
}
|
|
647
|
-
var REDACTED_SECRET_VALUE = "***REDACTED***";
|
|
648
|
-
function getSecretInputKeys(input) {
|
|
649
|
-
const keys = new Set;
|
|
650
|
-
if (!input)
|
|
651
|
-
return keys;
|
|
652
|
-
for (const [key, value] of Object.entries(input)) {
|
|
653
|
-
if (typeof value !== "string")
|
|
654
|
-
continue;
|
|
655
|
-
if (value.startsWith("secret.")) {
|
|
656
|
-
keys.add(key);
|
|
657
|
-
continue;
|
|
658
|
-
}
|
|
659
|
-
const envMatch = /^\$\{(.+)\}$/.exec(value);
|
|
660
|
-
if (envMatch?.[1] && isSensitiveKey(envMatch[1])) {
|
|
661
|
-
keys.add(key);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
return keys;
|
|
665
|
-
}
|
|
666
|
-
function redactSecretsForStorage(ctx, secretKeys) {
|
|
667
|
-
if (secretKeys.size === 0)
|
|
668
|
-
return ctx;
|
|
669
|
-
const inputBlock = ctx.input;
|
|
670
|
-
if (!inputBlock || typeof inputBlock !== "object")
|
|
671
|
-
return ctx;
|
|
672
|
-
const redactedInput = { ...inputBlock };
|
|
673
|
-
let touched = false;
|
|
674
|
-
for (const key of secretKeys) {
|
|
675
|
-
if (key in redactedInput) {
|
|
676
|
-
redactedInput[key] = REDACTED_SECRET_VALUE;
|
|
677
|
-
touched = true;
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
if (!touched)
|
|
681
|
-
return ctx;
|
|
682
|
-
return { ...ctx, input: redactedInput };
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// src/workflows/json-schema-validator.ts
|
|
686
|
-
function validateJsonSchema(schema, data) {
|
|
687
|
-
const errors = [];
|
|
688
|
-
validate(schema, data, "", errors);
|
|
689
|
-
return errors;
|
|
690
|
-
}
|
|
691
|
-
function validate(schema, data, path, errors) {
|
|
692
|
-
const prefix = path || "root";
|
|
693
|
-
if (schema.type !== undefined) {
|
|
694
|
-
const expected = schema.type;
|
|
695
|
-
if (!matchesType(expected, data)) {
|
|
696
|
-
errors.push(`${prefix}: expected type "${expected}", got ${typeOf(data)}`);
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
if (Array.isArray(schema.required) && typeof data === "object" && data !== null) {
|
|
701
|
-
const obj = data;
|
|
702
|
-
for (const key of schema.required) {
|
|
703
|
-
if (!(key in obj)) {
|
|
704
|
-
errors.push(`${prefix}: missing required property "${key}"`);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
if (Array.isArray(schema.enum)) {
|
|
709
|
-
const allowed = schema.enum;
|
|
710
|
-
if (!allowed.some((v) => JSON.stringify(v) === JSON.stringify(data))) {
|
|
711
|
-
errors.push(`${prefix}: value ${JSON.stringify(data)} not in enum [${allowed.map((v) => JSON.stringify(v)).join(", ")}]`);
|
|
712
|
-
return;
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
if ("const" in schema) {
|
|
716
|
-
if (JSON.stringify(data) !== JSON.stringify(schema.const)) {
|
|
717
|
-
errors.push(`${prefix}: value ${JSON.stringify(data)} does not match const ${JSON.stringify(schema.const)}`);
|
|
718
|
-
return;
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
if (schema.properties !== undefined && typeof schema.properties === "object" && schema.properties !== null && typeof data === "object" && data !== null) {
|
|
722
|
-
const props = schema.properties;
|
|
723
|
-
const obj = data;
|
|
724
|
-
for (const [key, subSchema] of Object.entries(props)) {
|
|
725
|
-
if (key in obj) {
|
|
726
|
-
validate(subSchema, obj[key], path ? `${path}.${key}` : key, errors);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
if (schema.items !== undefined && typeof schema.items === "object" && schema.items !== null && Array.isArray(data)) {
|
|
731
|
-
const itemSchema = schema.items;
|
|
732
|
-
for (let i = 0;i < data.length; i++) {
|
|
733
|
-
validate(itemSchema, data[i], `${prefix}[${i}]`, errors);
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
function matchesType(expected, data) {
|
|
738
|
-
switch (expected) {
|
|
739
|
-
case "string":
|
|
740
|
-
return typeof data === "string";
|
|
741
|
-
case "number":
|
|
742
|
-
return typeof data === "number";
|
|
743
|
-
case "boolean":
|
|
744
|
-
return typeof data === "boolean";
|
|
745
|
-
case "array":
|
|
746
|
-
return Array.isArray(data);
|
|
747
|
-
case "object":
|
|
748
|
-
return typeof data === "object" && data !== null && !Array.isArray(data);
|
|
749
|
-
default:
|
|
750
|
-
return true;
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
function typeOf(data) {
|
|
754
|
-
if (data === null)
|
|
755
|
-
return "null";
|
|
756
|
-
if (data === undefined)
|
|
757
|
-
return "undefined";
|
|
758
|
-
if (Array.isArray(data))
|
|
759
|
-
return "array";
|
|
760
|
-
return typeof data;
|
|
761
|
-
}
|
|
762
|
-
function isPlainObject(value) {
|
|
763
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
764
|
-
}
|
|
765
|
-
function findJsonSchemaShapeErrors(schema, path = "outputSchema") {
|
|
766
|
-
const errors = [];
|
|
767
|
-
if (!isPlainObject(schema)) {
|
|
768
|
-
errors.push(`${path}: must be a JSON Schema object, got ${typeOf(schema)}`);
|
|
769
|
-
return errors;
|
|
770
|
-
}
|
|
771
|
-
if (schema.type !== undefined && typeof schema.type !== "string") {
|
|
772
|
-
errors.push(`${path}.type: must be a string, got ${typeOf(schema.type)}`);
|
|
773
|
-
}
|
|
774
|
-
if (schema.required !== undefined && (!Array.isArray(schema.required) || !schema.required.every((v) => typeof v === "string"))) {
|
|
775
|
-
errors.push(`${path}.required: must be an array of strings`);
|
|
776
|
-
}
|
|
777
|
-
if (schema.enum !== undefined && !Array.isArray(schema.enum)) {
|
|
778
|
-
errors.push(`${path}.enum: must be an array`);
|
|
779
|
-
}
|
|
780
|
-
if (schema.properties !== undefined) {
|
|
781
|
-
if (!isPlainObject(schema.properties)) {
|
|
782
|
-
errors.push(`${path}.properties: must be an object mapping property names to schemas`);
|
|
783
|
-
} else {
|
|
784
|
-
for (const [key, value] of Object.entries(schema.properties)) {
|
|
785
|
-
errors.push(...findJsonSchemaShapeErrors(value, `${path}.properties.${key}`));
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
if (schema.items !== undefined) {
|
|
790
|
-
errors.push(...findJsonSchemaShapeErrors(schema.items, `${path}.items`));
|
|
791
|
-
}
|
|
792
|
-
return errors;
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// src/workflows/limits.ts
|
|
796
|
-
var DEFAULT_MAX_STEPS_PER_RUN = 500;
|
|
797
|
-
function getMaxWorkflowStepsPerRun() {
|
|
798
|
-
return Number(process.env.WORKFLOW_MAX_STEPS_PER_RUN) || DEFAULT_MAX_STEPS_PER_RUN;
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
// src/workflows/readiness.ts
|
|
802
|
-
async function findWorkflowReadinessProblems(definition, registry) {
|
|
803
|
-
const nodesByType = new Map;
|
|
804
|
-
for (const node of definition.nodes) {
|
|
805
|
-
if (!registry.has(node.type))
|
|
806
|
-
continue;
|
|
807
|
-
const group = nodesByType.get(node.type) ?? [];
|
|
808
|
-
group.push({ id: node.id, config: node.config });
|
|
809
|
-
nodesByType.set(node.type, group);
|
|
810
|
-
}
|
|
811
|
-
const problems = [];
|
|
812
|
-
for (const [type, nodes] of nodesByType) {
|
|
813
|
-
try {
|
|
814
|
-
for (const problem of await registry.get(type).checkReadiness(nodes)) {
|
|
815
|
-
problems.push({ type, ...problem });
|
|
816
|
-
}
|
|
817
|
-
} catch {
|
|
818
|
-
problems.push({
|
|
819
|
-
type,
|
|
820
|
-
nodeIds: nodes.map((node) => node.id),
|
|
821
|
-
message: `Could not check whether "${type}" nodes are ready to run`
|
|
822
|
-
});
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
return problems;
|
|
826
|
-
}
|
|
827
|
-
function describeNodes(problem) {
|
|
828
|
-
const noun = problem.nodeIds.length === 1 ? "node" : "nodes";
|
|
829
|
-
return `${problem.type} ${noun} ${problem.nodeIds.map((id) => `"${id}"`).join(", ")}`;
|
|
830
|
-
}
|
|
831
|
-
function formatReadinessWarning(problem) {
|
|
832
|
-
return `${problem.message} Affects ${describeNodes(problem)}. Runs of this workflow fail before any node executes until this is fixed.`;
|
|
833
|
-
}
|
|
834
|
-
function formatReadinessProblems(problems) {
|
|
835
|
-
return problems.map((problem) => `${problem.message} Needed by ${describeNodes(problem)}.`).join(" ");
|
|
836
|
-
}
|
|
837
|
-
function formatReadinessRunError(problems) {
|
|
838
|
-
return `Run not started, no node executed: ${formatReadinessProblems(problems)}`;
|
|
839
|
-
}
|
|
840
|
-
async function workflowSaveWarnings(definition, registry) {
|
|
841
|
-
return (await findWorkflowReadinessProblems(definition, registry)).map(formatReadinessWarning);
|
|
842
|
-
}
|
|
843
|
-
function withSaveWarnings(text, warnings) {
|
|
844
|
-
return warnings.length === 0 ? text : `${text} ${warnings.map((warning) => `Warning: ${warning}`).join(" ")}`;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// src/workflows/template.ts
|
|
848
|
-
init_template();
|
|
849
|
-
|
|
850
|
-
// src/workflows/validation.ts
|
|
851
|
-
function extractPassResult(executorType, output) {
|
|
852
|
-
if (!output || typeof output !== "object")
|
|
853
|
-
return false;
|
|
854
|
-
const o = output;
|
|
855
|
-
switch (executorType) {
|
|
856
|
-
case "validate":
|
|
857
|
-
return o.pass === true;
|
|
858
|
-
case "script":
|
|
859
|
-
return o.exitCode === 0;
|
|
860
|
-
case "property-match":
|
|
861
|
-
return o.passed === true;
|
|
862
|
-
case "raw-llm":
|
|
863
|
-
if (typeof o.result === "object" && o.result !== null) {
|
|
864
|
-
return o.result.pass === true;
|
|
865
|
-
}
|
|
866
|
-
return false;
|
|
867
|
-
default:
|
|
868
|
-
return o.pass === true || o.passed === true || o.exitCode === 0;
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
async function runStepValidation(registry, stepNode, stepOutput, context, meta) {
|
|
872
|
-
if (!stepNode.validation) {
|
|
873
|
-
return { outcome: "pass" };
|
|
874
|
-
}
|
|
875
|
-
const validation = stepNode.validation;
|
|
876
|
-
const executorType = validation.executor || "validate";
|
|
877
|
-
const executor = registry.get(executorType);
|
|
878
|
-
const validationConfig = {
|
|
879
|
-
targetNodeId: meta.nodeId,
|
|
880
|
-
...validation.config
|
|
881
|
-
};
|
|
882
|
-
const validationContext = {
|
|
883
|
-
...context,
|
|
884
|
-
[meta.nodeId]: stepOutput
|
|
885
|
-
};
|
|
886
|
-
const result = await executor.run({
|
|
887
|
-
config: validationConfig,
|
|
888
|
-
context: validationContext,
|
|
889
|
-
meta: {
|
|
890
|
-
...meta,
|
|
891
|
-
stepId: crypto.randomUUID()
|
|
892
|
-
}
|
|
893
|
-
});
|
|
894
|
-
const passed = result.status === "success" && extractPassResult(executorType, result.output);
|
|
895
|
-
if (passed) {
|
|
896
|
-
return { outcome: "pass", passed: true };
|
|
897
|
-
}
|
|
898
|
-
if (validation.mustPass) {
|
|
899
|
-
if (validation.retry) {
|
|
900
|
-
return {
|
|
901
|
-
outcome: "retry",
|
|
902
|
-
passed: false,
|
|
903
|
-
retryContext: {
|
|
904
|
-
previousOutput: stepOutput,
|
|
905
|
-
validationResult: result.output
|
|
906
|
-
}
|
|
907
|
-
};
|
|
908
|
-
}
|
|
909
|
-
return { outcome: "halt", passed: false };
|
|
910
|
-
}
|
|
911
|
-
return { outcome: "pass", passed: false };
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
// src/workflows/engine.ts
|
|
915
|
-
var DEFAULT_TIMEOUT_MS = 30000;
|
|
916
|
-
function maxIterations() {
|
|
917
|
-
return Number(process.env.WORKFLOW_MAX_ITERATIONS) || 100;
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
class TriggerSchemaError extends Error {
|
|
921
|
-
validationErrors;
|
|
922
|
-
constructor(validationErrors) {
|
|
923
|
-
super(`Trigger schema validation failed: ${validationErrors.join("; ")}`);
|
|
924
|
-
this.validationErrors = validationErrors;
|
|
925
|
-
this.name = "TriggerSchemaError";
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
async function resolveRenderedWorkflowInputs(input) {
|
|
929
|
-
const stringInputs = {};
|
|
930
|
-
const resolved = {};
|
|
931
|
-
for (const [key, value] of Object.entries(input)) {
|
|
932
|
-
if (typeof value === "string")
|
|
933
|
-
stringInputs[key] = value;
|
|
934
|
-
else
|
|
935
|
-
resolved[key] = value;
|
|
936
|
-
}
|
|
937
|
-
return { ...resolved, ...await resolveInputs(stringInputs) };
|
|
938
|
-
}
|
|
939
|
-
async function startWorkflowExecution2(workflow, triggerData, registry, options = {}) {
|
|
940
|
-
const preflight = preflightAutomation(workflowPreflightInput(workflow), await getAutomationSetupStates());
|
|
941
|
-
if (preflight.state === "needs_setup") {
|
|
942
|
-
const { runId, recorded } = await recordWorkflowPreflightFailure({
|
|
943
|
-
workflowId: workflow.id,
|
|
944
|
-
triggerType: options.triggerType ?? "manual",
|
|
945
|
-
triggerData,
|
|
946
|
-
failureReason: preflight.failureReason,
|
|
947
|
-
createdBy: options.requestedByUserId
|
|
948
|
-
});
|
|
949
|
-
if (recorded)
|
|
950
|
-
await notifyAutomationPreflightFailure(preflight);
|
|
951
|
-
return runId;
|
|
952
|
-
}
|
|
953
|
-
workflow = renderAutomationTokens(workflow, workflow.params ?? {});
|
|
954
|
-
if (workflow.triggerSchema) {
|
|
955
|
-
const validationErrors = validateJsonSchema(workflow.triggerSchema, triggerData);
|
|
956
|
-
if (validationErrors.length > 0) {
|
|
957
|
-
throw new TriggerSchemaError(validationErrors);
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
if (workflow.cooldown && await shouldSkipCooldown(workflow.id, workflow.cooldown)) {
|
|
961
|
-
const runId = crypto.randomUUID();
|
|
962
|
-
await createWorkflowRun2({
|
|
963
|
-
id: runId,
|
|
964
|
-
workflowId: workflow.id,
|
|
965
|
-
triggerType: options.triggerType ?? "manual",
|
|
966
|
-
triggerData,
|
|
967
|
-
createdBy: options.requestedByUserId,
|
|
968
|
-
parentStepId: options.parentStepId
|
|
969
|
-
});
|
|
970
|
-
await updateWorkflowRun2(runId, {
|
|
971
|
-
status: "skipped",
|
|
972
|
-
error: "cooldown",
|
|
973
|
-
finishedAt: new Date().toISOString()
|
|
974
|
-
});
|
|
975
|
-
return runId;
|
|
976
|
-
}
|
|
977
|
-
const runId = crypto.randomUUID();
|
|
978
|
-
await createWorkflowRun2({
|
|
979
|
-
id: runId,
|
|
980
|
-
workflowId: workflow.id,
|
|
981
|
-
triggerType: options.triggerType ?? "manual",
|
|
982
|
-
triggerData,
|
|
983
|
-
createdBy: options.requestedByUserId,
|
|
984
|
-
parentStepId: options.parentStepId
|
|
985
|
-
});
|
|
986
|
-
telemetry.workflow("started", {
|
|
987
|
-
workflowId: workflow.id,
|
|
988
|
-
nodeCount: workflow.definition.nodes.length,
|
|
989
|
-
triggerType: options.triggerType ?? "manual"
|
|
990
|
-
});
|
|
991
|
-
const notReady = await findWorkflowReadinessProblems(workflow.definition, registry);
|
|
992
|
-
if (notReady.length > 0) {
|
|
993
|
-
await updateWorkflowRun2(runId, {
|
|
994
|
-
status: "failed",
|
|
995
|
-
error: formatReadinessRunError(notReady),
|
|
996
|
-
finishedAt: new Date().toISOString()
|
|
997
|
-
});
|
|
998
|
-
return runId;
|
|
999
|
-
}
|
|
1000
|
-
const ctx = { trigger: triggerData };
|
|
1001
|
-
if (options.requestedByUserId) {
|
|
1002
|
-
ctx.swarm = { requestedByUserId: options.requestedByUserId };
|
|
1003
|
-
}
|
|
1004
|
-
if (workflow.dir || workflow.vcsRepo) {
|
|
1005
|
-
ctx.workflow = { dir: workflow.dir, vcsRepo: workflow.vcsRepo };
|
|
1006
|
-
}
|
|
1007
|
-
if (workflow.input) {
|
|
1008
|
-
try {
|
|
1009
|
-
const resolved = await resolveRenderedWorkflowInputs(workflow.input);
|
|
1010
|
-
Object.assign(ctx, { input: resolved });
|
|
1011
|
-
} catch (err) {
|
|
1012
|
-
await updateWorkflowRun2(runId, {
|
|
1013
|
-
status: "failed",
|
|
1014
|
-
error: `Input resolution failed: ${err}`,
|
|
1015
|
-
finishedAt: new Date().toISOString()
|
|
1016
|
-
});
|
|
1017
|
-
return runId;
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
const entryNodes = findEntryNodes(workflow.definition);
|
|
1021
|
-
const secretKeys = getSecretInputKeys(workflow.input);
|
|
1022
|
-
const walk = walkGraph(workflow.definition, runId, ctx, entryNodes, registry, workflow.id, secretKeys, options);
|
|
1023
|
-
if (options.parentStepId) {
|
|
1024
|
-
walk.catch((err) => console.error(`[workflows] Child workflow run ${runId} failed:`, err));
|
|
1025
|
-
return runId;
|
|
1026
|
-
}
|
|
1027
|
-
await walk;
|
|
1028
|
-
return runId;
|
|
1029
|
-
}
|
|
1030
|
-
var activeWalks = new Map;
|
|
1031
|
-
var executingSteps = new Set;
|
|
1032
|
-
async function failRunOnUnreadableReplay(runId, err, stepId) {
|
|
1033
|
-
if (!(err instanceof WorkflowReplayStateError))
|
|
1034
|
-
return false;
|
|
1035
|
-
const finishedAt = new Date().toISOString();
|
|
1036
|
-
console.error(`[workflow] ${err.message}`);
|
|
1037
|
-
if (stepId) {
|
|
1038
|
-
await updateWorkflowRunStep2(stepId, {
|
|
1039
|
-
status: "failed",
|
|
1040
|
-
error: err.message,
|
|
1041
|
-
finishedAt,
|
|
1042
|
-
nextRetryAt: null
|
|
1043
|
-
});
|
|
1044
|
-
}
|
|
1045
|
-
await updateWorkflowRun2(runId, { status: "failed", error: err.message, finishedAt });
|
|
1046
|
-
return true;
|
|
1047
|
-
}
|
|
1048
|
-
function isWorkflowRunActive(runId) {
|
|
1049
|
-
return activeWalks.has(runId);
|
|
1050
|
-
}
|
|
1051
|
-
function holdWorkflowRun(runId) {
|
|
1052
|
-
activeWalks.set(runId, (activeWalks.get(runId) ?? 0) + 1);
|
|
1053
|
-
let released = false;
|
|
1054
|
-
return () => {
|
|
1055
|
-
if (released)
|
|
1056
|
-
return;
|
|
1057
|
-
released = true;
|
|
1058
|
-
const remaining = activeWalks.get(runId) - 1;
|
|
1059
|
-
if (remaining === 0)
|
|
1060
|
-
activeWalks.delete(runId);
|
|
1061
|
-
else
|
|
1062
|
-
activeWalks.set(runId, remaining);
|
|
1063
|
-
};
|
|
1064
|
-
}
|
|
1065
|
-
async function walkGraph(def, runId, ctx, startNodes, registry, workflowId, secretKeys = new Set, options = {}) {
|
|
1066
|
-
const release = holdWorkflowRun(runId);
|
|
1067
|
-
try {
|
|
1068
|
-
await walkGraphOwned(def, runId, ctx, startNodes, registry, workflowId, secretKeys, options);
|
|
1069
|
-
} catch (err) {
|
|
1070
|
-
if (!await failRunOnUnreadableReplay(runId, err))
|
|
1071
|
-
throw err;
|
|
1072
|
-
} finally {
|
|
1073
|
-
release();
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
async function walkGraphOwned(def, runId, ctx, startNodes, registry, workflowId, secretKeys = new Set, options = {}) {
|
|
1077
|
-
if (!options.requestedByUserId) {
|
|
1078
|
-
options = {
|
|
1079
|
-
...options,
|
|
1080
|
-
requestedByUserId: (await getWorkflowRun2(runId))?.createdBy
|
|
1081
|
-
};
|
|
1082
|
-
}
|
|
1083
|
-
let nodeExecutionCount = 0;
|
|
1084
|
-
if (!("run" in ctx)) {
|
|
1085
|
-
ctx.run = { id: runId };
|
|
1086
|
-
}
|
|
1087
|
-
const { completedNodeIds } = await rehydrateCompletedStepOutputs(def, runId, ctx, registry);
|
|
1088
|
-
const { activeEdges } = await loadCompletedStepRouting(def, runId, completedNodeIds);
|
|
1089
|
-
const allSteps = await getWorkflowRunStepsByRunId2(runId);
|
|
1090
|
-
if (allSteps.length >= getMaxWorkflowStepsPerRun()) {
|
|
1091
|
-
await updateWorkflowRun2(runId, {
|
|
1092
|
-
status: "failed",
|
|
1093
|
-
error: `Circuit breaker: run exceeded ${getMaxWorkflowStepsPerRun()} total steps (WORKFLOW_MAX_STEPS_PER_RUN)`,
|
|
1094
|
-
finishedAt: new Date().toISOString()
|
|
1095
|
-
});
|
|
1096
|
-
return;
|
|
1097
|
-
}
|
|
1098
|
-
for (const step of allSteps) {
|
|
1099
|
-
if (step.status !== "waiting")
|
|
1100
|
-
continue;
|
|
1101
|
-
const successors = getSuccessors(def, step.nodeId);
|
|
1102
|
-
for (const succ of successors) {
|
|
1103
|
-
activeEdges.add(`${step.nodeId}→${succ.id}`);
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
const awaited = awaitedNodeIds(allSteps, activeEdges);
|
|
1107
|
-
let pendingNodes = startNodes.filter((n) => {
|
|
1108
|
-
const preds = getAllPredecessors(def, n.id);
|
|
1109
|
-
if (preds.length === 0) {
|
|
1110
|
-
return !completedNodeIds.has(n.id);
|
|
1111
|
-
}
|
|
1112
|
-
const activePreds = preds.filter((predId) => activeEdges.has(`${predId}→${n.id}`) || awaited.has(predId));
|
|
1113
|
-
const predsToCheck = activePreds.length > 0 ? activePreds : preds;
|
|
1114
|
-
return predsToCheck.every((p) => completedNodeIds.has(p));
|
|
1115
|
-
});
|
|
1116
|
-
const executedInThisWalk = new Set;
|
|
1117
|
-
while (pendingNodes.length > 0) {
|
|
1118
|
-
nodeExecutionCount += pendingNodes.length;
|
|
1119
|
-
if (nodeExecutionCount > maxIterations()) {
|
|
1120
|
-
await updateWorkflowRun2(runId, {
|
|
1121
|
-
status: "failed",
|
|
1122
|
-
error: `Max node executions (${maxIterations()}) exceeded — possible infinite loop`,
|
|
1123
|
-
finishedAt: new Date().toISOString()
|
|
1124
|
-
});
|
|
1125
|
-
return;
|
|
1126
|
-
}
|
|
1127
|
-
const results = await Promise.all(pendingNodes.map((node) => executeStep(def, runId, ctx, node, registry, workflowId, secretKeys, options).catch(async (err) => {
|
|
1128
|
-
await failRunOnUnreadableReplay(runId, err);
|
|
1129
|
-
return { outcome: "failed", successors: [] };
|
|
1130
|
-
})));
|
|
1131
|
-
const nextBatch = new Map;
|
|
1132
|
-
let hasWaiting = false;
|
|
1133
|
-
for (let i = 0;i < results.length; i++) {
|
|
1134
|
-
const result = results[i];
|
|
1135
|
-
if (result.outcome === "failed") {
|
|
1136
|
-
const currentRun = await getWorkflowRun2(runId);
|
|
1137
|
-
if (currentRun?.status === "failed")
|
|
1138
|
-
return;
|
|
1139
|
-
continue;
|
|
1140
|
-
}
|
|
1141
|
-
if (result.outcome === "waiting") {
|
|
1142
|
-
hasWaiting = true;
|
|
1143
|
-
continue;
|
|
1144
|
-
}
|
|
1145
|
-
const sourceNodeId = pendingNodes[i].id;
|
|
1146
|
-
completedNodeIds.add(sourceNodeId);
|
|
1147
|
-
executedInThisWalk.add(sourceNodeId);
|
|
1148
|
-
for (const succ of result.successors) {
|
|
1149
|
-
activeEdges.add(`${sourceNodeId}→${succ.id}`);
|
|
1150
|
-
nextBatch.set(succ.id, succ);
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
if (hasWaiting)
|
|
1154
|
-
return;
|
|
1155
|
-
const readyNext = [];
|
|
1156
|
-
const batchSteps = nextBatch.size > 0 ? await getWorkflowRunStepsByRunId2(runId) : [];
|
|
1157
|
-
const batchAwaited = awaitedNodeIds(batchSteps, activeEdges);
|
|
1158
|
-
const batchLatest = latestStepByNode(batchSteps);
|
|
1159
|
-
for (const [nodeId, node] of nextBatch) {
|
|
1160
|
-
if (executedInThisWalk.has(nodeId))
|
|
1161
|
-
continue;
|
|
1162
|
-
const allPreds = getAllPredecessors(def, nodeId);
|
|
1163
|
-
for (const predId of allPreds) {
|
|
1164
|
-
if (completedNodeIds.has(predId))
|
|
1165
|
-
continue;
|
|
1166
|
-
const latest = batchLatest.get(predId);
|
|
1167
|
-
if (latest?.status !== "completed")
|
|
1168
|
-
continue;
|
|
1169
|
-
completedNodeIds.add(predId);
|
|
1170
|
-
if (latest.output !== undefined)
|
|
1171
|
-
ctx[predId] = latest.output;
|
|
1172
|
-
}
|
|
1173
|
-
const activePreds = allPreds.filter((predId) => activeEdges.has(`${predId}→${nodeId}`) || batchAwaited.has(predId));
|
|
1174
|
-
const allActivePredsCompleted = activePreds.every((p) => completedNodeIds.has(p));
|
|
1175
|
-
if (allActivePredsCompleted) {
|
|
1176
|
-
readyNext.push(node);
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
pendingNodes = readyNext;
|
|
1180
|
-
}
|
|
1181
|
-
await getDbClient2().transaction(async () => {
|
|
1182
|
-
const run = await getWorkflowRun2(runId);
|
|
1183
|
-
if (!run || run.status !== "running")
|
|
1184
|
-
return;
|
|
1185
|
-
const finalSteps = await getWorkflowRunStepsByRunId2(runId);
|
|
1186
|
-
if (hasRunningStep(finalSteps))
|
|
1187
|
-
return;
|
|
1188
|
-
const hasWaitingSteps = finalSteps.some((s) => s.status === "waiting");
|
|
1189
|
-
const hasPendingRetries = finalSteps.some((s) => s.status === "failed" && s.nextRetryAt != null);
|
|
1190
|
-
const failedSteps = finalSteps.filter((s) => s.status === "failed" && s.nextRetryAt == null);
|
|
1191
|
-
const entryNodeIds = new Set(findEntryNodes(def).map((n) => n.id));
|
|
1192
|
-
const hasCompletedSteps = finalSteps.some((s) => s.status === "completed" && !entryNodeIds.has(s.nodeId));
|
|
1193
|
-
if (hasWaitingSteps) {
|
|
1194
|
-
await updateWorkflowRun2(runId, { status: "waiting" });
|
|
1195
|
-
} else if (!hasPendingRetries) {
|
|
1196
|
-
if (failedSteps.length > 0 && !hasCompletedSteps) {
|
|
1197
|
-
const failedNodeIds = failedSteps.map((s) => s.nodeId).join(", ");
|
|
1198
|
-
await updateWorkflowRun2(runId, {
|
|
1199
|
-
status: "failed",
|
|
1200
|
-
error: `All branches failed. Failed nodes: ${failedNodeIds}`,
|
|
1201
|
-
context: ctx,
|
|
1202
|
-
finishedAt: new Date().toISOString()
|
|
1203
|
-
});
|
|
1204
|
-
} else if (failedSteps.length > 0) {
|
|
1205
|
-
const failedNodeIds = failedSteps.map((s) => s.nodeId).join(", ");
|
|
1206
|
-
await updateWorkflowRun2(runId, {
|
|
1207
|
-
status: "completed",
|
|
1208
|
-
error: `Partial failure: nodes [${failedNodeIds}] failed (mustPass validation), but other branches completed successfully`,
|
|
1209
|
-
context: ctx,
|
|
1210
|
-
finishedAt: new Date().toISOString()
|
|
1211
|
-
});
|
|
1212
|
-
} else {
|
|
1213
|
-
await updateWorkflowRun2(runId, {
|
|
1214
|
-
status: "completed",
|
|
1215
|
-
context: ctx,
|
|
1216
|
-
finishedAt: new Date().toISOString()
|
|
1217
|
-
});
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
});
|
|
1221
|
-
}
|
|
1222
|
-
function latestStepByNode(steps) {
|
|
1223
|
-
const latest = new Map;
|
|
1224
|
-
for (const step of steps)
|
|
1225
|
-
latest.set(step.nodeId, step);
|
|
1226
|
-
return latest;
|
|
1227
|
-
}
|
|
1228
|
-
function awaitedNodeIds(steps, activeEdges) {
|
|
1229
|
-
const latest = latestStepByNode(steps);
|
|
1230
|
-
const awaited = new Set;
|
|
1231
|
-
for (const [nodeId, step] of latest) {
|
|
1232
|
-
if (step.status === "running" || step.status === "waiting")
|
|
1233
|
-
awaited.add(nodeId);
|
|
1234
|
-
}
|
|
1235
|
-
for (const edge of activeEdges) {
|
|
1236
|
-
const target = edge.slice(edge.indexOf("→") + 1);
|
|
1237
|
-
if (!latest.has(target))
|
|
1238
|
-
awaited.add(target);
|
|
1239
|
-
}
|
|
1240
|
-
return awaited;
|
|
1241
|
-
}
|
|
1242
|
-
function hasRunningStep(steps) {
|
|
1243
|
-
for (const step of latestStepByNode(steps).values()) {
|
|
1244
|
-
if (step.status === "running")
|
|
1245
|
-
return true;
|
|
1246
|
-
}
|
|
1247
|
-
return false;
|
|
1248
|
-
}
|
|
1249
|
-
async function rehydrateCompletedStepOutputs(def, runId, ctx, registry) {
|
|
1250
|
-
const completedNodeIds = new Set(await getCompletedStepNodeIds2(runId));
|
|
1251
|
-
const latestSteps = new Map;
|
|
1252
|
-
for (const nodeId of completedNodeIds) {
|
|
1253
|
-
if (resolveForeachParent(def, nodeId))
|
|
1254
|
-
continue;
|
|
1255
|
-
const step = await getLatestStepForNode2(runId, nodeId);
|
|
1256
|
-
if (!step)
|
|
1257
|
-
continue;
|
|
1258
|
-
if (step.output !== undefined) {
|
|
1259
|
-
const node = def.nodes.find((candidate) => candidate.id === nodeId);
|
|
1260
|
-
if (node && registry.has(node.type)) {
|
|
1261
|
-
const executor = registry.get(node.type);
|
|
1262
|
-
const parseResult = executor.outputSchema.safeParse(step.output);
|
|
1263
|
-
if (!parseResult.success) {
|
|
1264
|
-
console.warn(`[workflow] Recovery: step ${nodeId} output failed validation: ${parseResult.error.message}`);
|
|
1265
|
-
continue;
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
ctx[nodeId] = step.output;
|
|
1269
|
-
}
|
|
1270
|
-
latestSteps.set(nodeId, step);
|
|
1271
|
-
}
|
|
1272
|
-
return { completedNodeIds, latestSteps };
|
|
1273
|
-
}
|
|
1274
|
-
function getAllPredecessors(def, nodeId) {
|
|
1275
|
-
const preds = [];
|
|
1276
|
-
for (const node of def.nodes) {
|
|
1277
|
-
if (!node.next)
|
|
1278
|
-
continue;
|
|
1279
|
-
if (getNextTargets(node.next).includes(nodeId)) {
|
|
1280
|
-
preds.push(node.id);
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
return preds;
|
|
1284
|
-
}
|
|
1285
|
-
async function executeStep(def, runId, ctx, node, registry, workflowId, secretKeys = new Set, options = {}) {
|
|
1286
|
-
let claimedStepId;
|
|
1287
|
-
const claimStep = async () => {
|
|
1288
|
-
const run = await getWorkflowRun2(runId);
|
|
1289
|
-
if (!run || run.status !== "running" && run.status !== "waiting") {
|
|
1290
|
-
return { halted: true };
|
|
1291
|
-
}
|
|
1292
|
-
if (node.type !== "foreach") {
|
|
1293
|
-
const current = await getCurrentStepForNode2(runId, node.id, getAllPredecessors(def, node.id));
|
|
1294
|
-
if (current && (current.status !== "running" || executingSteps.has(current.id))) {
|
|
1295
|
-
return { current };
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
const iteration = await getStepCountForNode2(runId, node.id);
|
|
1299
|
-
const idempotencyKey = `${runId}:${node.id}:${iteration}`;
|
|
1300
|
-
const existingStep = await getStepByIdempotencyKey2(idempotencyKey);
|
|
1301
|
-
if (existingStep && (existingStep.status === "completed" || existingStep.status === "waiting")) {
|
|
1302
|
-
return { existingStep, stepId: existingStep.id, deduped: true };
|
|
1303
|
-
}
|
|
1304
|
-
const latestForeachStep = node.type === "foreach" ? await getLatestStepForNode2(runId, node.id) : undefined;
|
|
1305
|
-
const reusableForeachStep = latestForeachStep && !FOREACH_TERMINAL_STEP_STATUSES.has(latestForeachStep.status) ? latestForeachStep : undefined;
|
|
1306
|
-
const stepId = reusableForeachStep?.id ?? crypto.randomUUID();
|
|
1307
|
-
if (!reusableForeachStep) {
|
|
1308
|
-
await createWorkflowRunStep2({
|
|
1309
|
-
id: stepId,
|
|
1310
|
-
runId,
|
|
1311
|
-
nodeId: node.id,
|
|
1312
|
-
nodeType: node.type,
|
|
1313
|
-
input: redactSecretsForStorage(ctx, secretKeys),
|
|
1314
|
-
idempotencyKey
|
|
1315
|
-
});
|
|
1316
|
-
}
|
|
1317
|
-
claimedStepId = stepId;
|
|
1318
|
-
executingSteps.add(stepId);
|
|
1319
|
-
return { existingStep, stepId, deduped: false };
|
|
1320
|
-
};
|
|
1321
|
-
const dedup = await getDbClient2().transaction(claimStep).catch((err) => {
|
|
1322
|
-
if (claimedStepId)
|
|
1323
|
-
executingSteps.delete(claimedStepId);
|
|
1324
|
-
throw err;
|
|
1325
|
-
});
|
|
1326
|
-
if ("halted" in dedup)
|
|
1327
|
-
return { outcome: "completed", successors: [] };
|
|
1328
|
-
if ("current" in dedup && dedup.current) {
|
|
1329
|
-
if (dedup.current.status !== "completed")
|
|
1330
|
-
return { outcome: "waiting", successors: [] };
|
|
1331
|
-
ctx[node.id] = dedup.current.output;
|
|
1332
|
-
return { outcome: "completed", successors: [] };
|
|
1333
|
-
}
|
|
1334
|
-
if (dedup.deduped && dedup.existingStep) {
|
|
1335
|
-
if (dedup.existingStep.status === "completed") {
|
|
1336
|
-
ctx[node.id] = dedup.existingStep.output;
|
|
1337
|
-
const successors = getSuccessors(def, node.id);
|
|
1338
|
-
return { outcome: "completed", successors };
|
|
1339
|
-
}
|
|
1340
|
-
return { outcome: "waiting", successors: [] };
|
|
1341
|
-
}
|
|
1342
|
-
try {
|
|
1343
|
-
return await runClaimedStep(def, runId, ctx, node, registry, dedup.existingStep, dedup.stepId, workflowId, options);
|
|
1344
|
-
} catch (err) {
|
|
1345
|
-
if (await failRunOnUnreadableReplay(runId, err, dedup.stepId)) {
|
|
1346
|
-
return { outcome: "failed", successors: [] };
|
|
1347
|
-
}
|
|
1348
|
-
throw err;
|
|
1349
|
-
} finally {
|
|
1350
|
-
executingSteps.delete(dedup.stepId);
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
async function runClaimedStep(def, runId, ctx, node, registry, existingStep, stepId, workflowId, options = {}) {
|
|
1354
|
-
const executor = registry.get(node.type);
|
|
1355
|
-
const systemOneRetryProblems = systemOneRetryViolations(node);
|
|
1356
|
-
if (systemOneRetryProblems.length > 0) {
|
|
1357
|
-
await checkpointStepFailure(runId, stepId, systemOneRetryProblems.join("; "), 0);
|
|
1358
|
-
return { outcome: "failed", successors: [] };
|
|
1359
|
-
}
|
|
1360
|
-
const interpolationCtx = buildNodeInterpolationCtx(node, ctx);
|
|
1361
|
-
if (node.inputSchema) {
|
|
1362
|
-
const inputErrors = validateJsonSchema(node.inputSchema, interpolationCtx);
|
|
1363
|
-
if (inputErrors.length > 0) {
|
|
1364
|
-
const errorMsg = `Input schema validation failed: ${inputErrors.join("; ")}`;
|
|
1365
|
-
await checkpointStepFailure(runId, stepId, errorMsg, 0);
|
|
1366
|
-
throw new Error(errorMsg);
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
const {
|
|
1370
|
-
value: interpolatedValue,
|
|
1371
|
-
unresolved,
|
|
1372
|
-
scriptBodyUnresolved,
|
|
1373
|
-
strictUnresolved
|
|
1374
|
-
} = interpolateNodeConfig(node, interpolationCtx);
|
|
1375
|
-
const interpolatedConfig = interpolatedValue;
|
|
1376
|
-
const executionCtx = { ...ctx, ...interpolationCtx };
|
|
1377
|
-
if (scriptBodyUnresolved && scriptBodyUnresolved.length > 0) {
|
|
1378
|
-
const errorMsg = scriptBodyInterpolationError(node.id, scriptBodyUnresolved);
|
|
1379
|
-
await checkpointStepFailure(runId, stepId, errorMsg, 0);
|
|
1380
|
-
return { outcome: "failed", successors: [] };
|
|
1381
|
-
}
|
|
1382
|
-
if (strictUnresolved && strictUnresolved.length > 0) {
|
|
1383
|
-
await checkpointStepFailure(runId, stepId, systemOneUnresolvedError(node.id, strictUnresolved, node.type), 0);
|
|
1384
|
-
return { outcome: "failed", successors: [] };
|
|
1385
|
-
}
|
|
1386
|
-
if (unresolved.length > 0) {
|
|
1387
|
-
console.warn(`[workflow] Step ${node.id}: unresolved interpolation tokens: ${unresolved.join(", ")}`);
|
|
1388
|
-
await updateWorkflowRunStep2(stepId, {
|
|
1389
|
-
diagnostics: JSON.stringify({ unresolvedTokens: unresolved })
|
|
1390
|
-
});
|
|
1391
|
-
}
|
|
1392
|
-
const meta = {
|
|
1393
|
-
runId,
|
|
1394
|
-
stepId,
|
|
1395
|
-
nodeId: node.id,
|
|
1396
|
-
workflowId: workflowId || "",
|
|
1397
|
-
dryRun: false,
|
|
1398
|
-
requestedByUserId: options.requestedByUserId,
|
|
1399
|
-
inputCtx: interpolationCtx
|
|
1400
|
-
};
|
|
1401
|
-
const configuredTimeoutMs = typeof node.config?.timeoutMs === "number" ? node.config.timeoutMs : node.type === "script" && typeof node.config?.timeout === "number" ? node.config.timeout : undefined;
|
|
1402
|
-
const timeoutMs = configuredTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1403
|
-
let result;
|
|
1404
|
-
try {
|
|
1405
|
-
result = await Promise.race([
|
|
1406
|
-
executor.run({
|
|
1407
|
-
config: interpolatedConfig,
|
|
1408
|
-
context: executionCtx,
|
|
1409
|
-
meta
|
|
1410
|
-
}),
|
|
1411
|
-
timeoutPromise(timeoutMs)
|
|
1412
|
-
]);
|
|
1413
|
-
} catch (err) {
|
|
1414
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1415
|
-
const retryPolicy = node.retry || executor.retryPolicy;
|
|
1416
|
-
const currentRetryCount = existingStep?.retryCount || 0;
|
|
1417
|
-
const { shouldRetry } = await checkpointStepFailure(runId, stepId, errorMsg, currentRetryCount, retryPolicy);
|
|
1418
|
-
if (!shouldRetry) {
|
|
1419
|
-
throw err;
|
|
1420
|
-
}
|
|
1421
|
-
return { outcome: "completed", successors: [] };
|
|
1422
|
-
}
|
|
1423
|
-
if (result.status === "failed") {
|
|
1424
|
-
const retryPolicy = node.retry || executor.retryPolicy;
|
|
1425
|
-
const currentRetryCount = existingStep?.retryCount || 0;
|
|
1426
|
-
const { shouldRetry } = await checkpointStepFailure(runId, stepId, result.error || "Executor returned failed status", currentRetryCount, retryPolicy);
|
|
1427
|
-
if (result.output !== undefined) {
|
|
1428
|
-
await updateWorkflowRunStep2(stepId, { output: result.output });
|
|
1429
|
-
}
|
|
1430
|
-
if (!shouldRetry) {
|
|
1431
|
-
throw new Error(result.error || "Step execution failed");
|
|
1432
|
-
}
|
|
1433
|
-
return { outcome: "completed", successors: [] };
|
|
1434
|
-
}
|
|
1435
|
-
if ("async" in result && result.async) {
|
|
1436
|
-
const waiting = await checkpointStepWaiting(runId, stepId, ctx);
|
|
1437
|
-
if (waiting && result.waitFor === "workflow.child.finished") {
|
|
1438
|
-
const { resumeFromChildRun } = await import("./resume-z0fgjc38.js");
|
|
1439
|
-
await resumeFromChildRun(stepId, registry);
|
|
1440
|
-
}
|
|
1441
|
-
return { outcome: waiting ? "waiting" : "completed", successors: [] };
|
|
1442
|
-
}
|
|
1443
|
-
if (node.outputSchema && result.status === "success") {
|
|
1444
|
-
const outputErrors = validateJsonSchema(node.outputSchema, result.output);
|
|
1445
|
-
if (outputErrors.length > 0) {
|
|
1446
|
-
const errorMsg = `Output schema validation failed: ${outputErrors.join("; ")}`;
|
|
1447
|
-
await checkpointStepFailure(runId, stepId, errorMsg, 0);
|
|
1448
|
-
throw new Error(errorMsg);
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
let validationResult;
|
|
1452
|
-
if (node.validation) {
|
|
1453
|
-
validationResult = await runStepValidation(registry, node, result.output, executionCtx, meta);
|
|
1454
|
-
if (validationResult.outcome === "halt") {
|
|
1455
|
-
const errorMsg = "Validation failed (mustPass)";
|
|
1456
|
-
await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, { markRunFailed: false });
|
|
1457
|
-
return { outcome: "failed", successors: [] };
|
|
1458
|
-
}
|
|
1459
|
-
if (validationResult.outcome === "retry") {
|
|
1460
|
-
if (validationResult.retryContext) {
|
|
1461
|
-
const historyKey = `${node.id}_validations`;
|
|
1462
|
-
const existing = ctx[historyKey] || [];
|
|
1463
|
-
ctx[historyKey] = [...existing, validationResult.retryContext];
|
|
1464
|
-
}
|
|
1465
|
-
const retryPolicy = node.validation.retry || node.retry;
|
|
1466
|
-
const currentRetryCount = existingStep?.retryCount || 0;
|
|
1467
|
-
await checkpointStepFailure(runId, stepId, "Validation failed, retrying", currentRetryCount, retryPolicy);
|
|
1468
|
-
return { outcome: "completed", successors: [] };
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
result.nextPort = resolveValidationPort(node.next, result.nextPort, validationResult?.passed);
|
|
1472
|
-
await checkpointStep(runId, stepId, node.id, result, ctx);
|
|
1473
|
-
const successors = result.nextPort ? getSuccessors(def, node.id, result.nextPort) : getSuccessors(def, node.id);
|
|
1474
|
-
return { outcome: "completed", successors };
|
|
1475
|
-
}
|
|
1476
|
-
function findReadyNodes(def, completedNodeIds, activeEdges) {
|
|
1477
|
-
const predecessors = new Map;
|
|
1478
|
-
for (const node of def.nodes) {
|
|
1479
|
-
if (!predecessors.has(node.id)) {
|
|
1480
|
-
predecessors.set(node.id, new Set);
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
for (const node of def.nodes) {
|
|
1484
|
-
if (!node.next)
|
|
1485
|
-
continue;
|
|
1486
|
-
for (const target of getNextTargets(node.next)) {
|
|
1487
|
-
if (!predecessors.has(target)) {
|
|
1488
|
-
predecessors.set(target, new Set);
|
|
1489
|
-
}
|
|
1490
|
-
predecessors.get(target).add(node.id);
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
return def.nodes.filter((node) => {
|
|
1494
|
-
if (completedNodeIds.has(node.id))
|
|
1495
|
-
return false;
|
|
1496
|
-
const preds = predecessors.get(node.id);
|
|
1497
|
-
if (!preds || preds.size === 0)
|
|
1498
|
-
return true;
|
|
1499
|
-
if (activeEdges) {
|
|
1500
|
-
const activePreds = [...preds].filter((predId) => activeEdges.has(`${predId}→${node.id}`));
|
|
1501
|
-
if (activePreds.length === 0)
|
|
1502
|
-
return false;
|
|
1503
|
-
return activePreds.every((pred) => completedNodeIds.has(pred));
|
|
1504
|
-
}
|
|
1505
|
-
for (const pred of preds) {
|
|
1506
|
-
if (!completedNodeIds.has(pred))
|
|
1507
|
-
return false;
|
|
1508
|
-
}
|
|
1509
|
-
return true;
|
|
1510
|
-
});
|
|
1511
|
-
}
|
|
1512
|
-
function buildNodeInterpolationCtx(node, ctx) {
|
|
1513
|
-
const interpolationCtx = {};
|
|
1514
|
-
if (ctx.trigger !== undefined)
|
|
1515
|
-
interpolationCtx.trigger = ctx.trigger;
|
|
1516
|
-
if (ctx.input !== undefined)
|
|
1517
|
-
interpolationCtx.input = ctx.input;
|
|
1518
|
-
if (ctx.workflow !== undefined)
|
|
1519
|
-
interpolationCtx.workflow = ctx.workflow;
|
|
1520
|
-
if (ctx.swarm !== undefined)
|
|
1521
|
-
interpolationCtx.swarm = ctx.swarm;
|
|
1522
|
-
if (ctx.run !== undefined)
|
|
1523
|
-
interpolationCtx.run = ctx.run;
|
|
1524
|
-
if (!node.inputs)
|
|
1525
|
-
return interpolationCtx;
|
|
1526
|
-
for (const [localName, sourcePath] of Object.entries(node.inputs)) {
|
|
1527
|
-
const keys = sourcePath.split(".");
|
|
1528
|
-
let value = ctx;
|
|
1529
|
-
for (const key of keys) {
|
|
1530
|
-
if (value == null || typeof value !== "object") {
|
|
1531
|
-
value = undefined;
|
|
1532
|
-
break;
|
|
1533
|
-
}
|
|
1534
|
-
value = value[key];
|
|
1535
|
-
}
|
|
1536
|
-
interpolationCtx[localName] = value;
|
|
1537
|
-
}
|
|
1538
|
-
return interpolationCtx;
|
|
1539
|
-
}
|
|
1540
|
-
var SCRIPT_BODY_ALLOWED_SOURCES = new Set(["input", "workflow", "swarm", "run"]);
|
|
1541
|
-
function buildScriptBodyCtx(node, interpolationCtx) {
|
|
1542
|
-
const aliases = node.inputs ?? {};
|
|
1543
|
-
const safe = {};
|
|
1544
|
-
for (const [key, value] of Object.entries(interpolationCtx)) {
|
|
1545
|
-
const sourcePath = aliases[key];
|
|
1546
|
-
const root = sourcePath === undefined ? key : sourcePath.split(".")[0] ?? "";
|
|
1547
|
-
if (SCRIPT_BODY_ALLOWED_SOURCES.has(root))
|
|
1548
|
-
safe[key] = value;
|
|
1549
|
-
}
|
|
1550
|
-
return safe;
|
|
1551
|
-
}
|
|
1552
|
-
var SYSTEM_ONE_DATA_FIELDS = new Set(["state", "questions", "fields", "images"]);
|
|
1553
|
-
function interpolateNodeConfig(node, interpolationCtx) {
|
|
1554
|
-
if (isSystemOneNodeType(node.type)) {
|
|
1555
|
-
const rest = {};
|
|
1556
|
-
const dynamic = {};
|
|
1557
|
-
for (const [key, value] of Object.entries(node.config)) {
|
|
1558
|
-
if (SYSTEM_ONE_DATA_FIELDS.has(key))
|
|
1559
|
-
dynamic[key] = value;
|
|
1560
|
-
else
|
|
1561
|
-
rest[key] = value;
|
|
1562
|
-
}
|
|
1563
|
-
const restResult = deepInterpolate(rest, interpolationCtx);
|
|
1564
|
-
const dynamicResult = deepInterpolate(dynamic, interpolationCtx, { preserveRawTokens: true });
|
|
1565
|
-
const unresolved = [...restResult.unresolved, ...dynamicResult.unresolved];
|
|
1566
|
-
return {
|
|
1567
|
-
value: {
|
|
1568
|
-
...restResult.value,
|
|
1569
|
-
...dynamicResult.value
|
|
1570
|
-
},
|
|
1571
|
-
unresolved,
|
|
1572
|
-
strictUnresolved: unresolved
|
|
1573
|
-
};
|
|
1574
|
-
}
|
|
1575
|
-
if (node.type === "foreach" && Object.hasOwn(node.config, "over")) {
|
|
1576
|
-
const { over, body, ...configWithoutOverAndBody } = node.config;
|
|
1577
|
-
const configResult = deepInterpolate(configWithoutOverAndBody, interpolationCtx);
|
|
1578
|
-
const overResult = deepInterpolate(over, interpolationCtx, { preserveRawTokens: true });
|
|
1579
|
-
return {
|
|
1580
|
-
value: {
|
|
1581
|
-
...configResult.value,
|
|
1582
|
-
over: overResult.value,
|
|
1583
|
-
body
|
|
1584
|
-
},
|
|
1585
|
-
unresolved: [...configResult.unresolved, ...overResult.unresolved]
|
|
1586
|
-
};
|
|
1587
|
-
}
|
|
1588
|
-
if (node.type === "human-in-the-loop" && typeof node.config.questions === "string") {
|
|
1589
|
-
const { questions, ...configWithoutQuestions } = node.config;
|
|
1590
|
-
const configResult = deepInterpolate(configWithoutQuestions, interpolationCtx);
|
|
1591
|
-
const questionsResult = deepInterpolate(questions, interpolationCtx, {
|
|
1592
|
-
preserveRawTokens: true
|
|
1593
|
-
});
|
|
1594
|
-
return {
|
|
1595
|
-
value: {
|
|
1596
|
-
...configResult.value,
|
|
1597
|
-
questions: questionsResult.value
|
|
1598
|
-
},
|
|
1599
|
-
unresolved: [...configResult.unresolved, ...questionsResult.unresolved]
|
|
1600
|
-
};
|
|
1601
|
-
}
|
|
1602
|
-
if (node.type === "script" && typeof node.config.script === "string") {
|
|
1603
|
-
const { script, ...configWithoutScript } = node.config;
|
|
1604
|
-
const restResult = deepInterpolate(configWithoutScript, interpolationCtx);
|
|
1605
|
-
const scriptResult = deepInterpolate(script, buildScriptBodyCtx(node, interpolationCtx));
|
|
1606
|
-
return {
|
|
1607
|
-
value: {
|
|
1608
|
-
...restResult.value,
|
|
1609
|
-
script: scriptResult.unresolved.length > 0 ? script : scriptResult.value
|
|
1610
|
-
},
|
|
1611
|
-
unresolved: [...restResult.unresolved, ...scriptResult.unresolved],
|
|
1612
|
-
scriptBodyUnresolved: scriptResult.unresolved
|
|
1613
|
-
};
|
|
1614
|
-
}
|
|
1615
|
-
if (node.type !== "swarm-script" || !Object.hasOwn(node.config, "args")) {
|
|
1616
|
-
return deepInterpolate(node.config, interpolationCtx);
|
|
1617
|
-
}
|
|
1618
|
-
const { args, ...configWithoutArgs } = node.config;
|
|
1619
|
-
const configResult = deepInterpolate(configWithoutArgs, interpolationCtx);
|
|
1620
|
-
const argsResult = deepInterpolate(args, interpolationCtx, { preserveRawTokens: true });
|
|
1621
|
-
return {
|
|
1622
|
-
value: {
|
|
1623
|
-
...configResult.value,
|
|
1624
|
-
args: argsResult.value
|
|
1625
|
-
},
|
|
1626
|
-
unresolved: [...configResult.unresolved, ...argsResult.unresolved]
|
|
1627
|
-
};
|
|
1628
|
-
}
|
|
1629
|
-
function scriptBodyInterpolationError(nodeId, tokens) {
|
|
1630
|
-
const renderedTokens = [...new Set(tokens)].map((token) => `{{${token}}}`).join(", ");
|
|
1631
|
-
return `Script body interpolation failed for node "${nodeId}": unresolved or disallowed token(s): ${renderedTokens}. Executable source may interpolate only input, workflow, swarm, or run values. Pass dynamic trigger or upstream values through config.args and read them from argv.`;
|
|
1632
|
-
}
|
|
1633
|
-
function timeoutPromise(ms) {
|
|
1634
|
-
return new Promise((_, reject) => {
|
|
1635
|
-
setTimeout(() => reject(new Error(`Step timed out after ${ms}ms`)), ms);
|
|
1636
|
-
});
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
// src/workflows/approval-resolution.ts
|
|
1640
|
-
function shapeApprovalResolution(registry, definition, nodeId, parkedOutput, approval) {
|
|
1641
|
-
const node = definition.nodes.find((candidate) => candidate.id === nodeId);
|
|
1642
|
-
if (node && registry.has(node.type)) {
|
|
1643
|
-
const shaped = registry.get(node.type).resolveApproval(parkedOutput, approval);
|
|
1644
|
-
if (shaped)
|
|
1645
|
-
return shaped;
|
|
1646
|
-
}
|
|
1647
|
-
return {
|
|
1648
|
-
output: {
|
|
1649
|
-
requestId: approval.requestId,
|
|
1650
|
-
status: approval.status,
|
|
1651
|
-
responses: approval.responses
|
|
1652
|
-
},
|
|
1653
|
-
nextPort: approval.status === "timeout" ? "timeout" : approval.status === "rejected" ? "rejected" : "approved"
|
|
1654
|
-
};
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
// src/workflows/constants.ts
|
|
1658
|
-
var FAILED_TASK_OUTPUT_PREFIX = "[FAILED:";
|
|
1659
|
-
|
|
1660
|
-
// src/workflows/resume.ts
|
|
1661
|
-
init_db3();
|
|
1662
|
-
init_workflow_runs();
|
|
1663
|
-
init_secret_scrubber();
|
|
1664
|
-
init_event_bus();
|
|
1665
|
-
|
|
1666
|
-
// src/workflows/executors/sub-workflow.ts
|
|
1667
|
-
init_zod();
|
|
1668
|
-
init_workflow_runs();
|
|
1669
|
-
var SubWorkflowConfigSchema = object2({
|
|
1670
|
-
workflowId: string2().min(1).describe("Workflow to run as a child"),
|
|
1671
|
-
inputs: record2(string2(), unknown()).optional().describe("Trigger data for the child run, validated against its triggerSchema")
|
|
1672
|
-
});
|
|
1673
|
-
var SubWorkflowOutputSchema = object2({
|
|
1674
|
-
runId: string2(),
|
|
1675
|
-
outputs: record2(string2(), unknown()),
|
|
1676
|
-
error: string2().optional()
|
|
1677
|
-
});
|
|
1678
|
-
var MAX_SUB_WORKFLOW_DEPTH = 16;
|
|
1679
|
-
var NON_NODE_CONTEXT_KEYS = new Set(["trigger", "input", "swarm", "workflow"]);
|
|
1680
|
-
function childRunOutcome(child) {
|
|
1681
|
-
if (child.status === "running" || child.status === "waiting")
|
|
1682
|
-
return null;
|
|
1683
|
-
if (child.status !== "completed") {
|
|
1684
|
-
const reason = child.error ? `: ${child.error}` : "";
|
|
1685
|
-
return { error: `Child workflow run ${child.id} ${child.status}${reason}` };
|
|
1686
|
-
}
|
|
1687
|
-
const outputs = {};
|
|
1688
|
-
for (const [key, value] of Object.entries(child.context ?? {})) {
|
|
1689
|
-
if (!NON_NODE_CONTEXT_KEYS.has(key))
|
|
1690
|
-
outputs[key] = value;
|
|
1691
|
-
}
|
|
1692
|
-
return { output: { runId: child.id, outputs, error: child.error } };
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
class SubWorkflowExecutor extends BaseExecutor {
|
|
1696
|
-
registry;
|
|
1697
|
-
type = "sub-workflow";
|
|
1698
|
-
mode = "async";
|
|
1699
|
-
configSchema = SubWorkflowConfigSchema;
|
|
1700
|
-
outputSchema = SubWorkflowOutputSchema;
|
|
1701
|
-
constructor(deps, registry) {
|
|
1702
|
-
super(deps);
|
|
1703
|
-
this.registry = registry;
|
|
1704
|
-
}
|
|
1705
|
-
async execute(config, _context, meta) {
|
|
1706
|
-
const { db } = this.deps;
|
|
1707
|
-
const existingId = await getChildWorkflowRunId(meta.stepId);
|
|
1708
|
-
const existing = existingId ? await db.getWorkflowRun(existingId) : null;
|
|
1709
|
-
if (existing)
|
|
1710
|
-
return this.settle(existing);
|
|
1711
|
-
const lineage = await getWorkflowRunLineage(meta.runId, MAX_SUB_WORKFLOW_DEPTH);
|
|
1712
|
-
if (lineage.length > MAX_SUB_WORKFLOW_DEPTH) {
|
|
1713
|
-
return {
|
|
1714
|
-
status: "failed",
|
|
1715
|
-
error: `Sub-workflow nesting exceeds ${MAX_SUB_WORKFLOW_DEPTH} levels`
|
|
1716
|
-
};
|
|
1717
|
-
}
|
|
1718
|
-
if (config.workflowId === meta.workflowId || lineage.includes(config.workflowId)) {
|
|
1719
|
-
return {
|
|
1720
|
-
status: "failed",
|
|
1721
|
-
error: `Sub-workflow recursion: workflow ${config.workflowId} is already running in this run's ancestry`
|
|
1722
|
-
};
|
|
1723
|
-
}
|
|
1724
|
-
const workflow = await db.getWorkflow(config.workflowId);
|
|
1725
|
-
if (!workflow)
|
|
1726
|
-
return { status: "failed", error: `Workflow ${config.workflowId} not found` };
|
|
1727
|
-
if (!workflow.enabled) {
|
|
1728
|
-
return { status: "failed", error: `Workflow ${config.workflowId} is disabled` };
|
|
1729
|
-
}
|
|
1730
|
-
const childRunId = await startWorkflowExecution2(workflow, config.inputs ?? {}, this.registry, {
|
|
1731
|
-
requestedByUserId: meta.requestedByUserId,
|
|
1732
|
-
parentStepId: meta.stepId
|
|
1733
|
-
});
|
|
1734
|
-
const child = await db.getWorkflowRun(childRunId);
|
|
1735
|
-
if (!child)
|
|
1736
|
-
return { status: "failed", error: `Child workflow run ${childRunId} not found` };
|
|
1737
|
-
return this.settle(child);
|
|
1738
|
-
}
|
|
1739
|
-
settle(child) {
|
|
1740
|
-
const outcome = childRunOutcome(child);
|
|
1741
|
-
if (!outcome) {
|
|
1742
|
-
return {
|
|
1743
|
-
status: "success",
|
|
1744
|
-
async: true,
|
|
1745
|
-
waitFor: "workflow.child.finished",
|
|
1746
|
-
correlationId: child.id
|
|
1747
|
-
};
|
|
1748
|
-
}
|
|
1749
|
-
if ("error" in outcome)
|
|
1750
|
-
return { status: "failed", error: outcome.error };
|
|
1751
|
-
return { status: "success", output: outcome.output, nextPort: "success" };
|
|
1752
|
-
}
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
// src/workflows/executors/wait.ts
|
|
1756
|
-
init_zod();
|
|
1757
|
-
|
|
1758
|
-
// src/workflows/wait-filter.ts
|
|
1759
|
-
var PARAM_SHADOW_KEYS = [
|
|
1760
|
-
"require",
|
|
1761
|
-
"process",
|
|
1762
|
-
"Bun",
|
|
1763
|
-
"globalThis",
|
|
1764
|
-
"global",
|
|
1765
|
-
"fetch",
|
|
1766
|
-
"setTimeout",
|
|
1767
|
-
"setInterval"
|
|
1768
|
-
];
|
|
1769
|
-
var PARAM_SHADOW_VALUES = PARAM_SHADOW_KEYS.map(() => {
|
|
1770
|
-
return;
|
|
1771
|
-
});
|
|
1772
|
-
var BODY_SHADOW_KEYS = ["eval", "Function", "AsyncFunction"];
|
|
1773
|
-
var BODY_SHADOW_PREAMBLE = BODY_SHADOW_KEYS.map((k) => `var ${k} = undefined;`).join(" ");
|
|
1774
|
-
var FILTER_TIMEOUT_MS = 50;
|
|
1775
|
-
function getByDotPath(obj, path) {
|
|
1776
|
-
if (obj === null || typeof obj !== "object") {
|
|
1777
|
-
return { found: false, value: undefined };
|
|
1778
|
-
}
|
|
1779
|
-
const segments = path.split(".");
|
|
1780
|
-
let cursor = obj;
|
|
1781
|
-
for (const seg of segments) {
|
|
1782
|
-
if (cursor === null || typeof cursor !== "object") {
|
|
1783
|
-
return { found: false, value: undefined };
|
|
1784
|
-
}
|
|
1785
|
-
const rec = cursor;
|
|
1786
|
-
if (!(seg in rec)) {
|
|
1787
|
-
return { found: false, value: undefined };
|
|
1788
|
-
}
|
|
1789
|
-
cursor = rec[seg];
|
|
1790
|
-
}
|
|
1791
|
-
return { found: true, value: cursor };
|
|
1792
|
-
}
|
|
1793
|
-
function deepEqual(a, b) {
|
|
1794
|
-
if (a === b)
|
|
1795
|
-
return true;
|
|
1796
|
-
if (typeof a !== typeof b)
|
|
1797
|
-
return false;
|
|
1798
|
-
if (a === null || b === null)
|
|
1799
|
-
return false;
|
|
1800
|
-
if (Array.isArray(a)) {
|
|
1801
|
-
if (!Array.isArray(b))
|
|
1802
|
-
return false;
|
|
1803
|
-
if (a.length !== b.length)
|
|
1804
|
-
return false;
|
|
1805
|
-
for (let i = 0;i < a.length; i++) {
|
|
1806
|
-
if (!deepEqual(a[i], b[i]))
|
|
1807
|
-
return false;
|
|
1808
|
-
}
|
|
1809
|
-
return true;
|
|
1810
|
-
}
|
|
1811
|
-
if (typeof a === "object" && typeof b === "object") {
|
|
1812
|
-
const aKeys = Object.keys(a);
|
|
1813
|
-
const bKeys = Object.keys(b);
|
|
1814
|
-
if (aKeys.length !== bKeys.length)
|
|
1815
|
-
return false;
|
|
1816
|
-
for (const k of aKeys) {
|
|
1817
|
-
if (!deepEqual(a[k], b[k])) {
|
|
1818
|
-
return false;
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
return true;
|
|
1822
|
-
}
|
|
1823
|
-
return false;
|
|
1824
|
-
}
|
|
1825
|
-
function matchObjectFilter(payload, filter) {
|
|
1826
|
-
for (const [key, expected] of Object.entries(filter)) {
|
|
1827
|
-
const { found, value } = getByDotPath(payload, key);
|
|
1828
|
-
if (!found)
|
|
1829
|
-
return false;
|
|
1830
|
-
if (!deepEqual(value, expected))
|
|
1831
|
-
return false;
|
|
1832
|
-
}
|
|
1833
|
-
return true;
|
|
1834
|
-
}
|
|
1835
|
-
async function runWithTimeout(fn) {
|
|
1836
|
-
return new Promise((resolve) => {
|
|
1837
|
-
let settled = false;
|
|
1838
|
-
const timer = setTimeout(() => {
|
|
1839
|
-
if (settled)
|
|
1840
|
-
return;
|
|
1841
|
-
settled = true;
|
|
1842
|
-
resolve(null);
|
|
1843
|
-
}, FILTER_TIMEOUT_MS);
|
|
1844
|
-
queueMicrotask(() => {
|
|
1845
|
-
if (settled)
|
|
1846
|
-
return;
|
|
1847
|
-
try {
|
|
1848
|
-
const v = fn();
|
|
1849
|
-
const then = v !== null && typeof v === "object" ? v.then : undefined;
|
|
1850
|
-
if (typeof then === "function") {
|
|
1851
|
-
then.call(v, undefined, () => {});
|
|
1852
|
-
settled = true;
|
|
1853
|
-
clearTimeout(timer);
|
|
1854
|
-
resolve({ __asyncRejected: true });
|
|
1855
|
-
return;
|
|
1856
|
-
}
|
|
1857
|
-
if (settled)
|
|
1858
|
-
return;
|
|
1859
|
-
settled = true;
|
|
1860
|
-
clearTimeout(timer);
|
|
1861
|
-
resolve(v);
|
|
1862
|
-
} catch {
|
|
1863
|
-
if (settled)
|
|
1864
|
-
return;
|
|
1865
|
-
settled = true;
|
|
1866
|
-
clearTimeout(timer);
|
|
1867
|
-
resolve(null);
|
|
1868
|
-
}
|
|
1869
|
-
});
|
|
1870
|
-
});
|
|
1871
|
-
}
|
|
1872
|
-
var ASYNC_SENTINEL = "__asyncRejected";
|
|
1873
|
-
function isAsyncSentinel(value) {
|
|
1874
|
-
return value !== null && typeof value === "object" && value[ASYNC_SENTINEL] === true;
|
|
1875
|
-
}
|
|
1876
|
-
function compileStringFilter(filter) {
|
|
1877
|
-
if (filter.length > 2048) {
|
|
1878
|
-
throw new Error(`wait filter source exceeds 2KB cap (${filter.length} bytes)`);
|
|
1879
|
-
}
|
|
1880
|
-
let fn;
|
|
1881
|
-
try {
|
|
1882
|
-
fn = new Function(...PARAM_SHADOW_KEYS, "payload", `${BODY_SHADOW_PREAMBLE} return (${filter})(payload);`);
|
|
1883
|
-
} catch (err) {
|
|
1884
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1885
|
-
throw new Error(`wait filter compile error: ${msg}`);
|
|
1886
|
-
}
|
|
1887
|
-
return (payload) => fn(...PARAM_SHADOW_VALUES, payload);
|
|
1888
|
-
}
|
|
1889
|
-
function nullifyPrototypes(value) {
|
|
1890
|
-
if (value === null || typeof value !== "object")
|
|
1891
|
-
return value;
|
|
1892
|
-
if (Array.isArray(value)) {
|
|
1893
|
-
return value.map(nullifyPrototypes);
|
|
1894
|
-
}
|
|
1895
|
-
const out = Object.create(null);
|
|
1896
|
-
for (const [k, v] of Object.entries(value)) {
|
|
1897
|
-
out[k] = nullifyPrototypes(v);
|
|
1898
|
-
}
|
|
1899
|
-
return out;
|
|
1900
|
-
}
|
|
1901
|
-
async function matchesFilter(payload, filter) {
|
|
1902
|
-
if (filter === undefined || filter === null)
|
|
1903
|
-
return true;
|
|
1904
|
-
if (typeof filter === "object" && !Array.isArray(filter)) {
|
|
1905
|
-
return matchObjectFilter(payload, filter);
|
|
1906
|
-
}
|
|
1907
|
-
if (typeof filter === "string") {
|
|
1908
|
-
let userFn;
|
|
1909
|
-
try {
|
|
1910
|
-
userFn = compileStringFilter(filter);
|
|
1911
|
-
} catch {
|
|
1912
|
-
return false;
|
|
1913
|
-
}
|
|
1914
|
-
let cloned;
|
|
1915
|
-
try {
|
|
1916
|
-
const cloneOnce = structuredClone(payload);
|
|
1917
|
-
cloned = nullifyPrototypes(cloneOnce);
|
|
1918
|
-
} catch {
|
|
1919
|
-
try {
|
|
1920
|
-
cloned = nullifyPrototypes(payload);
|
|
1921
|
-
} catch {
|
|
1922
|
-
cloned = payload;
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
const result = await runWithTimeout(() => userFn(cloned));
|
|
1926
|
-
if (result === null)
|
|
1927
|
-
return false;
|
|
1928
|
-
if (isAsyncSentinel(result))
|
|
1929
|
-
return false;
|
|
1930
|
-
return !!result;
|
|
1931
|
-
}
|
|
1932
|
-
return false;
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
// src/workflows/executors/wait.ts
|
|
1936
|
-
var WaitConfigSchema = discriminatedUnion("mode", [
|
|
1937
|
-
object2({
|
|
1938
|
-
mode: literal2("time"),
|
|
1939
|
-
durationMs: number2().int().min(1).max(31536000000).describe("Wait duration in milliseconds (effective resolution ~5s)")
|
|
1940
|
-
}),
|
|
1941
|
-
object2({
|
|
1942
|
-
mode: literal2("event"),
|
|
1943
|
-
eventName: string2().min(1),
|
|
1944
|
-
filter: union2([record2(string2(), unknown()), string2().max(2048)]).optional(),
|
|
1945
|
-
scope: _enum2(["run", "global"]).default("run"),
|
|
1946
|
-
timeoutMs: number2().int().min(1).max(31536000000).optional().describe("Timeout in milliseconds (effective resolution ~5s) — when reached, routes via 'timeout' port")
|
|
1947
|
-
})
|
|
1948
|
-
]);
|
|
1949
|
-
var WaitOutputSchema = object2({
|
|
1950
|
-
waitId: string2().uuid(),
|
|
1951
|
-
mode: _enum2(["time", "event"]),
|
|
1952
|
-
firedAt: string2().nullable(),
|
|
1953
|
-
payload: unknown().optional()
|
|
1954
|
-
});
|
|
1955
|
-
|
|
1956
|
-
class WaitExecutor extends BaseExecutor {
|
|
1957
|
-
type = "wait";
|
|
1958
|
-
mode = "async";
|
|
1959
|
-
configSchema = WaitConfigSchema;
|
|
1960
|
-
outputSchema = WaitOutputSchema;
|
|
1961
|
-
async execute(config, _context, meta) {
|
|
1962
|
-
const { db } = this.deps;
|
|
1963
|
-
const existing = await db.getWaitStateByStepId(meta.stepId);
|
|
1964
|
-
if (existing) {
|
|
1965
|
-
if (existing.status !== "pending") {
|
|
1966
|
-
const nextPort = computeNextPort(existing.mode, existing.status);
|
|
1967
|
-
return {
|
|
1968
|
-
status: "success",
|
|
1969
|
-
output: {
|
|
1970
|
-
waitId: existing.id,
|
|
1971
|
-
mode: existing.mode,
|
|
1972
|
-
firedAt: existing.resolvedAt,
|
|
1973
|
-
payload: existing.firedPayload ?? undefined
|
|
1974
|
-
},
|
|
1975
|
-
nextPort
|
|
1976
|
-
};
|
|
1977
|
-
}
|
|
1978
|
-
return {
|
|
1979
|
-
status: "success",
|
|
1980
|
-
async: true,
|
|
1981
|
-
waitFor: "wait.fired",
|
|
1982
|
-
correlationId: existing.id
|
|
1983
|
-
};
|
|
1984
|
-
}
|
|
1985
|
-
if (config.mode === "time") {
|
|
1986
|
-
const waitId = crypto.randomUUID();
|
|
1987
|
-
const wakeUpAt = new Date(Date.now() + config.durationMs).toISOString();
|
|
1988
|
-
await db.createWaitState({
|
|
1989
|
-
id: waitId,
|
|
1990
|
-
workflowRunId: meta.runId,
|
|
1991
|
-
workflowRunStepId: meta.stepId,
|
|
1992
|
-
mode: "time",
|
|
1993
|
-
wakeUpAt
|
|
1994
|
-
});
|
|
1995
|
-
return {
|
|
1996
|
-
status: "success",
|
|
1997
|
-
async: true,
|
|
1998
|
-
waitFor: "wait.fired",
|
|
1999
|
-
correlationId: waitId
|
|
2000
|
-
};
|
|
2001
|
-
}
|
|
2002
|
-
if (typeof config.filter === "string") {
|
|
2003
|
-
compileStringFilter(config.filter);
|
|
2004
|
-
}
|
|
2005
|
-
const waitId = crypto.randomUUID();
|
|
2006
|
-
const expiresAt = config.timeoutMs ? new Date(Date.now() + config.timeoutMs).toISOString() : null;
|
|
2007
|
-
await db.createWaitState({
|
|
2008
|
-
id: waitId,
|
|
2009
|
-
workflowRunId: meta.runId,
|
|
2010
|
-
workflowRunStepId: meta.stepId,
|
|
2011
|
-
mode: "event",
|
|
2012
|
-
eventName: config.eventName,
|
|
2013
|
-
eventFilter: config.filter ?? null,
|
|
2014
|
-
expiresAt,
|
|
2015
|
-
scope: config.scope
|
|
2016
|
-
});
|
|
2017
|
-
subscribeWaitToBus2(waitId, config.eventName);
|
|
2018
|
-
return {
|
|
2019
|
-
status: "success",
|
|
2020
|
-
async: true,
|
|
2021
|
-
waitFor: "wait.fired",
|
|
2022
|
-
correlationId: waitId
|
|
2023
|
-
};
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
function computeNextPort(mode, status) {
|
|
2027
|
-
if (mode === "time")
|
|
2028
|
-
return "default";
|
|
2029
|
-
return status === "timeout" ? "timeout" : "event";
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
// src/workflows/task-step-routing.ts
|
|
2033
|
-
init_db3();
|
|
2034
|
-
init_types();
|
|
2035
|
-
async function isTaskBoundToStep(taskId, stepId) {
|
|
2036
|
-
const task = await getTaskById2(taskId);
|
|
2037
|
-
return task?.workflowRunStepId === stepId;
|
|
2038
|
-
}
|
|
2039
|
-
async function failStepAndRunIfWaiting(stepId, runId, reason, ownerTaskId) {
|
|
2040
|
-
const now = new Date().toISOString();
|
|
2041
|
-
return await getDbClient2().transaction(async () => {
|
|
2042
|
-
const current = await getWorkflowRunStep2(stepId);
|
|
2043
|
-
if (!current || current.status !== "waiting")
|
|
2044
|
-
return false;
|
|
2045
|
-
if (ownerTaskId && !await isTaskBoundToStep(ownerTaskId, stepId))
|
|
2046
|
-
return false;
|
|
2047
|
-
await updateWorkflowRunStep2(stepId, {
|
|
2048
|
-
status: "failed",
|
|
2049
|
-
error: reason,
|
|
2050
|
-
finishedAt: now
|
|
2051
|
-
});
|
|
2052
|
-
await updateWorkflowRun2(runId, {
|
|
2053
|
-
status: "failed",
|
|
2054
|
-
error: reason,
|
|
2055
|
-
finishedAt: now
|
|
2056
|
-
});
|
|
2057
|
-
return true;
|
|
2058
|
-
});
|
|
2059
|
-
}
|
|
2060
|
-
async function checkpointPortStepAndResolveSuccessors(def, runId, stepId, nodeId, output, nextPort, ctx) {
|
|
2061
|
-
return await getDbClient2().transaction(async () => {
|
|
2062
|
-
const current = await getWorkflowRunStep2(stepId);
|
|
2063
|
-
if (!current || current.status !== "waiting")
|
|
2064
|
-
return { claimed: false, successors: [] };
|
|
2065
|
-
const run = await getWorkflowRun2(runId);
|
|
2066
|
-
if (!run || run.status !== "waiting" && run.status !== "running") {
|
|
2067
|
-
return { claimed: false, successors: [] };
|
|
2068
|
-
}
|
|
2069
|
-
await checkpointStep(runId, stepId, nodeId, { output, nextPort }, ctx);
|
|
2070
|
-
await updateWorkflowRun2(runId, { status: "running" });
|
|
2071
|
-
return { claimed: true, successors: getSuccessors(def, nodeId, nextPort) };
|
|
2072
|
-
});
|
|
2073
|
-
}
|
|
2074
|
-
var SUPERSEDE_RETRY_POLICY = {
|
|
2075
|
-
maxRetries: 1,
|
|
2076
|
-
strategy: "static",
|
|
2077
|
-
baseDelayMs: 30000,
|
|
2078
|
-
maxDelayMs: 30000
|
|
2079
|
-
};
|
|
2080
|
-
async function scheduleTaskStepRetry(def, runId, step, taskId, reason) {
|
|
2081
|
-
const node = def.nodes.find((n) => n.id === step.nodeId);
|
|
2082
|
-
if (!node || node.type !== "agent-task")
|
|
2083
|
-
return "not-eligible";
|
|
2084
|
-
let policy;
|
|
2085
|
-
if (node.retry) {
|
|
2086
|
-
const parsed = RetryPolicySchema.safeParse(node.retry);
|
|
2087
|
-
if (!parsed.success)
|
|
2088
|
-
return "not-eligible";
|
|
2089
|
-
policy = parsed.data;
|
|
2090
|
-
} else {
|
|
2091
|
-
if (!await isSupersedeCut(taskId, reason))
|
|
2092
|
-
return "not-eligible";
|
|
2093
|
-
policy = SUPERSEDE_RETRY_POLICY;
|
|
2094
|
-
}
|
|
2095
|
-
return await getDbClient2().transaction(async () => {
|
|
2096
|
-
const current = await getWorkflowRunStep2(step.id);
|
|
2097
|
-
if (!current || current.status !== "waiting")
|
|
2098
|
-
return "not-claimed";
|
|
2099
|
-
if (!await isTaskBoundToStep(taskId, step.id))
|
|
2100
|
-
return "not-claimed";
|
|
2101
|
-
if (current.retryCount >= policy.maxRetries)
|
|
2102
|
-
return "not-eligible";
|
|
2103
|
-
await detachTaskFromWorkflowRunStep2(taskId);
|
|
2104
|
-
await checkpointStepFailure(runId, step.id, reason, current.retryCount, policy);
|
|
2105
|
-
return "scheduled";
|
|
2106
|
-
});
|
|
2107
|
-
}
|
|
2108
|
-
async function isSupersedeCut(taskId, reason) {
|
|
2109
|
-
if (reason === SUPERSEDED_WORKFLOW_TASK_REASON)
|
|
2110
|
-
return true;
|
|
2111
|
-
const task = await getTaskById2(taskId);
|
|
2112
|
-
return task?.failureReason === SUPERSEDED_WORKFLOW_TASK_REASON;
|
|
2113
|
-
}
|
|
2114
|
-
var UNCLAIMED = {
|
|
2115
|
-
claimed: false,
|
|
2116
|
-
foreachChild: false,
|
|
2117
|
-
joined: false,
|
|
2118
|
-
successors: []
|
|
2119
|
-
};
|
|
2120
|
-
async function completeTaskStepAndResolveSuccessors(def, runId, step, output, ctx, failureReason, ownerTaskId) {
|
|
2121
|
-
return await getDbClient2().transaction(async () => {
|
|
2122
|
-
const current = await getWorkflowRunStep2(step.id);
|
|
2123
|
-
if (!current || current.status !== "waiting")
|
|
2124
|
-
return UNCLAIMED;
|
|
2125
|
-
if (ownerTaskId && !await isTaskBoundToStep(ownerTaskId, step.id))
|
|
2126
|
-
return UNCLAIMED;
|
|
2127
|
-
const foreachParent = resolveForeachParent(def, step.nodeId);
|
|
2128
|
-
if (foreachParent) {
|
|
2129
|
-
await updateWorkflowRunStep2(step.id, {
|
|
2130
|
-
status: "completed",
|
|
2131
|
-
output,
|
|
2132
|
-
...failureReason !== undefined ? { error: failureReason } : {},
|
|
2133
|
-
finishedAt: new Date().toISOString()
|
|
2134
|
-
});
|
|
2135
|
-
const join = await joinForeach(def, runId, step, ctx);
|
|
2136
|
-
await updateWorkflowRun2(runId, { status: "running" });
|
|
2137
|
-
return {
|
|
2138
|
-
claimed: true,
|
|
2139
|
-
foreachChild: true,
|
|
2140
|
-
joined: join.joined,
|
|
2141
|
-
successors: join.successors
|
|
2142
|
-
};
|
|
2143
|
-
}
|
|
2144
|
-
await checkpointStep(runId, step.id, step.nodeId, { output }, ctx);
|
|
2145
|
-
await updateWorkflowRun2(runId, { status: "running" });
|
|
2146
|
-
return {
|
|
2147
|
-
claimed: true,
|
|
2148
|
-
foreachChild: false,
|
|
2149
|
-
joined: true,
|
|
2150
|
-
successors: getSuccessors(def, step.nodeId)
|
|
2151
|
-
};
|
|
2152
|
-
});
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
// src/workflows/resume.ts
|
|
2156
|
-
function setupWorkflowResumeListener2(eventBus, registry) {
|
|
2157
|
-
const onTaskCompleted = async (data) => {
|
|
2158
|
-
try {
|
|
2159
|
-
const event = data;
|
|
2160
|
-
if (!event.workflowRunId || !event.workflowRunStepId)
|
|
2161
|
-
return;
|
|
2162
|
-
await resumeFromTaskCompletion(event, registry);
|
|
2163
|
-
} catch (err) {
|
|
2164
|
-
console.error("[workflows] Resume from task completion failed:", err);
|
|
2165
|
-
}
|
|
2166
|
-
};
|
|
2167
|
-
eventBus.on("task.completed", onTaskCompleted);
|
|
2168
|
-
const onTaskFailed = async (data) => {
|
|
2169
|
-
try {
|
|
2170
|
-
const event = data;
|
|
2171
|
-
if (!event.workflowRunId || !event.workflowRunStepId)
|
|
2172
|
-
return;
|
|
2173
|
-
await handleTaskFailure(event, event.failureReason ?? "Task failed", registry, {
|
|
2174
|
-
retryable: true
|
|
2175
|
-
});
|
|
2176
|
-
} catch (err) {
|
|
2177
|
-
console.error("[workflows] Handle task failure error:", err);
|
|
2178
|
-
}
|
|
2179
|
-
};
|
|
2180
|
-
eventBus.on("task.failed", onTaskFailed);
|
|
2181
|
-
const onTaskCancelled = async (data) => {
|
|
2182
|
-
try {
|
|
2183
|
-
const event = data;
|
|
2184
|
-
if (!event.workflowRunId || !event.workflowRunStepId)
|
|
2185
|
-
return;
|
|
2186
|
-
await handleTaskFailure(event, "Task was cancelled", registry, { retryable: false });
|
|
2187
|
-
} catch (err) {
|
|
2188
|
-
console.error("[workflows] Handle task cancellation error:", err);
|
|
2189
|
-
}
|
|
2190
|
-
};
|
|
2191
|
-
eventBus.on("task.cancelled", onTaskCancelled);
|
|
2192
|
-
const onApprovalResolved = async (data) => {
|
|
2193
|
-
try {
|
|
2194
|
-
const event = data;
|
|
2195
|
-
if (!event.workflowRunId || !event.workflowRunStepId)
|
|
2196
|
-
return;
|
|
2197
|
-
await resumeFromApprovalResolution(event, registry);
|
|
2198
|
-
} catch (err) {
|
|
2199
|
-
console.error("[workflows] Resume from approval resolution failed:", err);
|
|
2200
|
-
}
|
|
2201
|
-
};
|
|
2202
|
-
eventBus.on("approval.resolved", onApprovalResolved);
|
|
2203
|
-
const onChildRunFinished = async (data) => {
|
|
2204
|
-
try {
|
|
2205
|
-
const { parentStepId } = data;
|
|
2206
|
-
if (!parentStepId)
|
|
2207
|
-
return;
|
|
2208
|
-
await resumeFromChildRun2(parentStepId, registry);
|
|
2209
|
-
} catch (err) {
|
|
2210
|
-
console.error("[workflows] Resume from child workflow run failed:", err);
|
|
2211
|
-
}
|
|
2212
|
-
};
|
|
2213
|
-
eventBus.on("workflow.child.finished", onChildRunFinished);
|
|
2214
|
-
return () => {
|
|
2215
|
-
eventBus.off("task.completed", onTaskCompleted);
|
|
2216
|
-
eventBus.off("task.failed", onTaskFailed);
|
|
2217
|
-
eventBus.off("task.cancelled", onTaskCancelled);
|
|
2218
|
-
eventBus.off("approval.resolved", onApprovalResolved);
|
|
2219
|
-
eventBus.off("workflow.child.finished", onChildRunFinished);
|
|
2220
|
-
};
|
|
2221
|
-
}
|
|
2222
|
-
async function failClosedOnUnreadableReplay(runId, stepId, resume) {
|
|
2223
|
-
try {
|
|
2224
|
-
await resume();
|
|
2225
|
-
} catch (err) {
|
|
2226
|
-
if (!await failRunOnUnreadableReplay(runId, err, stepId))
|
|
2227
|
-
throw err;
|
|
2228
|
-
}
|
|
2229
|
-
}
|
|
2230
|
-
async function resumeFromTaskCompletion(event, registry) {
|
|
2231
|
-
await failClosedOnUnreadableReplay(event.workflowRunId, event.workflowRunStepId, () => resumeFromTaskCompletionUnguarded(event, registry));
|
|
2232
|
-
}
|
|
2233
|
-
async function resumeFromTaskCompletionUnguarded(event, registry) {
|
|
2234
|
-
const run = await getWorkflowRun2(event.workflowRunId);
|
|
2235
|
-
if (!run || run.status !== "waiting" && run.status !== "running")
|
|
2236
|
-
return;
|
|
2237
|
-
const step = await getWorkflowRunStep2(event.workflowRunStepId);
|
|
2238
|
-
if (!step || step.status !== "waiting")
|
|
2239
|
-
return;
|
|
2240
|
-
if (await isStaleTaskEvent(step.id, event))
|
|
2241
|
-
return;
|
|
2242
|
-
const workflow = await getWorkflow2(run.workflowId);
|
|
2243
|
-
if (!workflow)
|
|
2244
|
-
return;
|
|
2245
|
-
const ctx = run.context ?? {};
|
|
2246
|
-
let taskOutput = event.output;
|
|
2247
|
-
if (event.output) {
|
|
2248
|
-
try {
|
|
2249
|
-
const parsed = JSON.parse(event.output);
|
|
2250
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
2251
|
-
taskOutput = parsed;
|
|
2252
|
-
}
|
|
2253
|
-
} catch {}
|
|
2254
|
-
}
|
|
2255
|
-
const stepOutput = { taskId: event.taskId, taskOutput };
|
|
2256
|
-
const routing = await completeTaskStepAndResolveSuccessors(workflow.definition, run.id, step, stepOutput, ctx);
|
|
2257
|
-
if (!routing.claimed)
|
|
2258
|
-
return;
|
|
2259
|
-
const successors = routing.successors;
|
|
2260
|
-
if (successors.length > 0) {
|
|
2261
|
-
const secretKeys = getSecretInputKeys(workflow.input);
|
|
2262
|
-
await walkGraph(workflow.definition, run.id, ctx, successors, registry, workflow.id, secretKeys);
|
|
2263
|
-
} else {
|
|
2264
|
-
await finalizeOrWait2(run.id);
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2267
|
-
async function finalizeOrWait2(runId) {
|
|
2268
|
-
await getDbClient2().transaction(async () => {
|
|
2269
|
-
const steps = await getWorkflowRunStepsByRunId2(runId);
|
|
2270
|
-
if (hasRunningStep(steps)) {
|
|
2271
|
-
const run = await getWorkflowRun2(runId);
|
|
2272
|
-
if (run?.status === "waiting")
|
|
2273
|
-
await updateWorkflowRun2(runId, { status: "running" });
|
|
2274
|
-
return;
|
|
2275
|
-
}
|
|
2276
|
-
const hasWaiting = steps.some((s) => s.status === "waiting" || s.status === "failed" && s.nextRetryAt != null);
|
|
2277
|
-
if (hasWaiting) {
|
|
2278
|
-
await updateWorkflowRun2(runId, { status: "waiting" });
|
|
2279
|
-
} else {
|
|
2280
|
-
await updateWorkflowRun2(runId, {
|
|
2281
|
-
status: "completed",
|
|
2282
|
-
finishedAt: new Date().toISOString()
|
|
2283
|
-
});
|
|
2284
|
-
}
|
|
2285
|
-
});
|
|
2286
|
-
}
|
|
2287
|
-
async function handleTaskFailure(event, reason, registry, options) {
|
|
2288
|
-
await failClosedOnUnreadableReplay(event.workflowRunId, event.workflowRunStepId, () => handleTaskFailureUnguarded(event, reason, registry, options));
|
|
2289
|
-
}
|
|
2290
|
-
async function handleTaskFailureUnguarded(event, reason, registry, options) {
|
|
2291
|
-
const run = await getWorkflowRun2(event.workflowRunId);
|
|
2292
|
-
if (!run || run.status !== "waiting" && run.status !== "running")
|
|
2293
|
-
return;
|
|
2294
|
-
const step = await getWorkflowRunStep2(event.workflowRunStepId);
|
|
2295
|
-
if (!step || step.status !== "waiting")
|
|
2296
|
-
return;
|
|
2297
|
-
if (await isStaleTaskEvent(step.id, event))
|
|
2298
|
-
return;
|
|
2299
|
-
const workflow = await getWorkflow2(run.workflowId);
|
|
2300
|
-
if (!workflow)
|
|
2301
|
-
return;
|
|
2302
|
-
if (options.retryable) {
|
|
2303
|
-
const retry = await scheduleTaskStepRetry(workflow.definition, run.id, step, event.taskId, reason);
|
|
2304
|
-
if (retry === "scheduled") {
|
|
2305
|
-
console.log(`[workflows] Task ${event.taskId} failed; step ${step.nodeId} of run ${run.id} queued for retry ${step.retryCount + 1}`);
|
|
2306
|
-
return;
|
|
2307
|
-
}
|
|
2308
|
-
if (retry === "not-claimed")
|
|
2309
|
-
return;
|
|
2310
|
-
}
|
|
2311
|
-
const onFailure = workflow.definition.onNodeFailure ?? "fail";
|
|
2312
|
-
if (onFailure === "fail") {
|
|
2313
|
-
await markRunFailed(event, reason);
|
|
2314
|
-
return;
|
|
2315
|
-
}
|
|
2316
|
-
const ctx = run.context ?? {};
|
|
2317
|
-
const stepOutput = {
|
|
2318
|
-
taskId: event.taskId,
|
|
2319
|
-
taskOutput: `${FAILED_TASK_OUTPUT_PREFIX} ${reason}] This node failed or was cancelled.`
|
|
2320
|
-
};
|
|
2321
|
-
const routing = await completeTaskStepAndResolveSuccessors(workflow.definition, run.id, step, stepOutput, ctx, reason);
|
|
2322
|
-
if (!routing.claimed)
|
|
2323
|
-
return;
|
|
2324
|
-
const successors = routing.successors;
|
|
2325
|
-
if (successors.length > 0) {
|
|
2326
|
-
const secretKeys = getSecretInputKeys(workflow.input);
|
|
2327
|
-
await walkGraph(workflow.definition, run.id, ctx, successors, registry, workflow.id, secretKeys);
|
|
2328
|
-
} else {
|
|
2329
|
-
await finalizeOrWait2(run.id);
|
|
2330
|
-
}
|
|
2331
|
-
}
|
|
2332
|
-
async function isStaleTaskEvent(stepId, event) {
|
|
2333
|
-
if (!event.taskId)
|
|
2334
|
-
return false;
|
|
2335
|
-
const boundTask = await getTaskByWorkflowRunStepId2(stepId);
|
|
2336
|
-
return boundTask != null && boundTask.id !== event.taskId;
|
|
2337
|
-
}
|
|
2338
|
-
async function markRunFailed(event, reason) {
|
|
2339
|
-
await failStepAndRunIfWaiting(event.workflowRunStepId, event.workflowRunId, reason);
|
|
2340
|
-
}
|
|
2341
|
-
async function retryFailedRun2(runId, registry) {
|
|
2342
|
-
const run = await getWorkflowRun2(runId);
|
|
2343
|
-
if (!run || run.status !== "failed")
|
|
2344
|
-
throw new Error("Run is not in failed state");
|
|
2345
|
-
const workflow = await getWorkflow2(run.workflowId);
|
|
2346
|
-
if (!workflow)
|
|
2347
|
-
throw new Error("Workflow not found");
|
|
2348
|
-
const steps = await getWorkflowRunStepsByRunId2(runId);
|
|
2349
|
-
const failedStep = steps.find((s) => s.status === "failed");
|
|
2350
|
-
if (!failedStep)
|
|
2351
|
-
throw new Error("No failed step found");
|
|
2352
|
-
const completedNodeIds = new Set(await getCompletedStepNodeIds2(runId));
|
|
2353
|
-
const notReady = await findWorkflowReadinessProblems({ nodes: workflow.definition.nodes.filter((node) => !completedNodeIds.has(node.id)) }, registry);
|
|
2354
|
-
if (notReady.length > 0) {
|
|
2355
|
-
throw new Error(`Retry not started, run left failed: ${formatReadinessProblems(notReady)}`);
|
|
2356
|
-
}
|
|
2357
|
-
const { activeEdges } = await loadCompletedStepRouting(workflow.definition, runId, completedNodeIds);
|
|
2358
|
-
const foreachParent = resolveForeachParent(workflow.definition, failedStep.nodeId);
|
|
2359
|
-
const failedNode = foreachParent ?? workflow.definition.nodes.find((n) => n.id === failedStep.nodeId);
|
|
2360
|
-
if (!failedNode)
|
|
2361
|
-
throw new Error(`Node ${failedStep.nodeId} not found in workflow definition`);
|
|
2362
|
-
const hasStructuralPredecessor = workflow.definition.nodes.some((node) => node.next && getNextTargets(node.next).includes(failedNode.id));
|
|
2363
|
-
const hasActivePredecessor = [...activeEdges].some((edge) => edge.endsWith(`→${failedNode.id}`));
|
|
2364
|
-
const shouldRetryFailedNode = foreachParent != null || !hasStructuralPredecessor || hasActivePredecessor;
|
|
2365
|
-
const ctx = run.context ?? {};
|
|
2366
|
-
const claimed = await getDbClient2().transaction(async () => {
|
|
2367
|
-
const current = await getWorkflowRun2(runId);
|
|
2368
|
-
if (!current || current.status !== "failed")
|
|
2369
|
-
return false;
|
|
2370
|
-
await updateWorkflowRunStep2(failedStep.id, {
|
|
2371
|
-
status: shouldRetryFailedNode ? "pending" : "cancelled",
|
|
2372
|
-
error: shouldRetryFailedNode ? null : "Skipped on retry because its branch was not active"
|
|
2373
|
-
});
|
|
2374
|
-
await updateWorkflowRun2(runId, { status: "running", error: null, context: ctx });
|
|
2375
|
-
return true;
|
|
2376
|
-
});
|
|
2377
|
-
if (!claimed)
|
|
2378
|
-
throw new Error("Run is not in failed state");
|
|
2379
|
-
const liveNodeIds = new Set(steps.filter((s) => s.status === "running" || s.status === "waiting").map((s) => s.nodeId));
|
|
2380
|
-
const readyNodes = findReadyNodes(workflow.definition, completedNodeIds, activeEdges).filter((n) => n.id === failedNode.id || !liveNodeIds.has(n.id));
|
|
2381
|
-
const nodesToRun = !shouldRetryFailedNode || readyNodes.some((n) => n.id === failedNode.id) ? readyNodes : [failedNode, ...readyNodes];
|
|
2382
|
-
const secretKeys = getSecretInputKeys(workflow.input);
|
|
2383
|
-
await walkGraph(workflow.definition, runId, ctx, nodesToRun, registry, workflow.id, secretKeys);
|
|
2384
|
-
}
|
|
2385
|
-
async function cancelWorkflowRunRows2(runId, cancelReason) {
|
|
2386
|
-
const terminalStatuses = ["completed", "failed", "cancelled", "skipped"];
|
|
2387
|
-
const now = new Date().toISOString();
|
|
2388
|
-
return await getDbClient2().transaction(async () => {
|
|
2389
|
-
const current = await getWorkflowRun2(runId);
|
|
2390
|
-
if (!current || terminalStatuses.includes(current.status))
|
|
2391
|
-
return false;
|
|
2392
|
-
const steps = await getWorkflowRunStepsByRunId2(runId);
|
|
2393
|
-
for (const step of steps) {
|
|
2394
|
-
if (terminalStatuses.includes(step.status))
|
|
2395
|
-
continue;
|
|
2396
|
-
const task = await getTaskByWorkflowRunStepId2(step.id);
|
|
2397
|
-
if (task) {
|
|
2398
|
-
await cancelTask2(task.id, cancelReason);
|
|
2399
|
-
}
|
|
2400
|
-
await updateWorkflowRunStep2(step.id, {
|
|
2401
|
-
status: "cancelled",
|
|
2402
|
-
error: cancelReason,
|
|
2403
|
-
finishedAt: now
|
|
2404
|
-
});
|
|
2405
|
-
}
|
|
2406
|
-
await cancelPendingApprovalRequestsForRun2(runId, cancelReason);
|
|
2407
|
-
await updateWorkflowRun2(runId, {
|
|
2408
|
-
status: "cancelled",
|
|
2409
|
-
error: cancelReason,
|
|
2410
|
-
finishedAt: now
|
|
2411
|
-
});
|
|
2412
|
-
return true;
|
|
2413
|
-
});
|
|
2414
|
-
}
|
|
2415
|
-
async function cancelWorkflowRun2(runId, reason) {
|
|
2416
|
-
const run = await getWorkflowRun2(runId);
|
|
2417
|
-
if (!run)
|
|
2418
|
-
throw new Error("Workflow run not found");
|
|
2419
|
-
const terminalStatuses = ["completed", "failed", "cancelled", "skipped"];
|
|
2420
|
-
if (terminalStatuses.includes(run.status) && run.status !== "cancelled") {
|
|
2421
|
-
throw new Error(`Cannot cancel run in '${run.status}' state`);
|
|
2422
|
-
}
|
|
2423
|
-
const cancelReason = reason ?? "Cancelled by user";
|
|
2424
|
-
const applied = await cancelWorkflowRunRows2(runId, cancelReason);
|
|
2425
|
-
const cancelledApprovals = await listCancelledApprovalRequestsForRun2(runId);
|
|
2426
|
-
const latestRun = await getWorkflowRun2(runId);
|
|
2427
|
-
if (!applied && latestRun?.status !== "cancelled") {
|
|
2428
|
-
throw new Error(`Cannot cancel run in '${latestRun?.status ?? "missing"}' state`);
|
|
2429
|
-
}
|
|
2430
|
-
await postApprovalCancellationUpdates(cancelledApprovals, applied ? cancelReason : latestRun?.error ?? cancelReason);
|
|
2431
|
-
}
|
|
2432
|
-
async function resumeFromApprovalResolution(event, registry) {
|
|
2433
|
-
await failClosedOnUnreadableReplay(event.workflowRunId, event.workflowRunStepId, () => resumeFromApprovalResolutionUnguarded(event, registry));
|
|
2434
|
-
}
|
|
2435
|
-
async function resumeFromApprovalResolutionUnguarded(event, registry) {
|
|
2436
|
-
const run = await getWorkflowRun2(event.workflowRunId);
|
|
2437
|
-
if (!run || run.status !== "waiting" && run.status !== "running")
|
|
2438
|
-
return;
|
|
2439
|
-
const step = await getWorkflowRunStep2(event.workflowRunStepId);
|
|
2440
|
-
if (!step || step.status !== "waiting")
|
|
2441
|
-
return;
|
|
2442
|
-
const workflow = await getWorkflow2(run.workflowId);
|
|
2443
|
-
if (!workflow)
|
|
2444
|
-
return;
|
|
2445
|
-
if (event.status === "cancelled") {
|
|
2446
|
-
console.warn(`[workflows] approval ${event.requestId} is cancelled; step ${event.workflowRunStepId} stays waiting for the run cancel path`);
|
|
2447
|
-
return;
|
|
2448
|
-
}
|
|
2449
|
-
const ctx = run.context ?? {};
|
|
2450
|
-
const { output: stepOutput, nextPort } = shapeApprovalResolution(registry, workflow.definition, step.nodeId, step.output, { requestId: event.requestId, status: event.status, responses: event.responses });
|
|
2451
|
-
const routing = await checkpointPortStepAndResolveSuccessors(workflow.definition, run.id, step.id, step.nodeId, stepOutput, nextPort, ctx);
|
|
2452
|
-
if (!routing.claimed)
|
|
2453
|
-
return;
|
|
2454
|
-
const successors = routing.successors;
|
|
2455
|
-
if (successors.length > 0) {
|
|
2456
|
-
const secretKeys = getSecretInputKeys(workflow.input);
|
|
2457
|
-
await walkGraph(workflow.definition, run.id, ctx, successors, registry, workflow.id, secretKeys);
|
|
2458
|
-
} else {
|
|
2459
|
-
await finalizeOrWait2(run.id);
|
|
2460
|
-
}
|
|
2461
|
-
}
|
|
2462
|
-
async function resumeWaitState2(waitId, status, payload, registry) {
|
|
2463
|
-
const cappedPayload = capPayload(payload);
|
|
2464
|
-
const result = await resolveWaitState2(waitId, { status, firedPayload: cappedPayload });
|
|
2465
|
-
if (!result.updated || !result.row)
|
|
2466
|
-
return;
|
|
2467
|
-
const waitRow = result.row;
|
|
2468
|
-
await failClosedOnUnreadableReplay(waitRow.workflowRunId, waitRow.workflowRunStepId, () => resumeClaimedWait(waitRow, status, cappedPayload, registry));
|
|
2469
|
-
}
|
|
2470
|
-
async function recoverResolvedWaitState2(waitId, registry) {
|
|
2471
|
-
const waitRow = await getWaitStateById2(waitId);
|
|
2472
|
-
if (!waitRow)
|
|
2473
|
-
return;
|
|
2474
|
-
const status = waitRow.status;
|
|
2475
|
-
if (status !== "fired" && status !== "timeout")
|
|
2476
|
-
return;
|
|
2477
|
-
await failClosedOnUnreadableReplay(waitRow.workflowRunId, waitRow.workflowRunStepId, () => resumeClaimedWait(waitRow, status, waitRow.firedPayload, registry));
|
|
2478
|
-
}
|
|
2479
|
-
async function resumeClaimedWait(waitRow, status, cappedPayload, registry) {
|
|
2480
|
-
const run = await getWorkflowRun2(waitRow.workflowRunId);
|
|
2481
|
-
if (!run || run.status !== "waiting" && run.status !== "running")
|
|
2482
|
-
return;
|
|
2483
|
-
const step = await getWorkflowRunStep2(waitRow.workflowRunStepId);
|
|
2484
|
-
if (!step || step.status !== "waiting")
|
|
2485
|
-
return;
|
|
2486
|
-
const workflow = await getWorkflow2(run.workflowId);
|
|
2487
|
-
if (!workflow)
|
|
2488
|
-
return;
|
|
2489
|
-
const nextPort = computeNextPort(waitRow.mode, status);
|
|
2490
|
-
const ctx = run.context ?? {};
|
|
2491
|
-
const stepOutput = {
|
|
2492
|
-
waitId: waitRow.id,
|
|
2493
|
-
mode: waitRow.mode,
|
|
2494
|
-
firedAt: waitRow.resolvedAt,
|
|
2495
|
-
payload: cappedPayload === undefined ? undefined : cappedPayload
|
|
2496
|
-
};
|
|
2497
|
-
const routing = await checkpointPortStepAndResolveSuccessors(workflow.definition, run.id, step.id, step.nodeId, stepOutput, nextPort, ctx);
|
|
2498
|
-
if (!routing.claimed)
|
|
2499
|
-
return;
|
|
2500
|
-
if (waitRow.mode === "event" && waitRow.eventName) {
|
|
2501
|
-
pruneWaitFromBus(waitRow.id, waitRow.eventName);
|
|
2502
|
-
}
|
|
2503
|
-
const successors = routing.successors;
|
|
2504
|
-
if (successors.length > 0) {
|
|
2505
|
-
const secretKeys = getSecretInputKeys(workflow.input);
|
|
2506
|
-
await walkGraph(workflow.definition, run.id, ctx, successors, registry, workflow.id, secretKeys);
|
|
2507
|
-
} else {
|
|
2508
|
-
await finalizeOrWait2(run.id);
|
|
2509
|
-
}
|
|
2510
|
-
}
|
|
2511
|
-
async function resumeFromChildRun2(parentStepId, registry) {
|
|
2512
|
-
const step = await getWorkflowRunStep2(parentStepId);
|
|
2513
|
-
if (!step || step.status !== "waiting" || step.nodeType !== "sub-workflow")
|
|
2514
|
-
return false;
|
|
2515
|
-
const childId = await getChildWorkflowRunId(parentStepId);
|
|
2516
|
-
const child = childId ? await getWorkflowRun2(childId) : null;
|
|
2517
|
-
const outcome = child ? childRunOutcome(child) : { error: `Child workflow run of step ${parentStepId} was deleted` };
|
|
2518
|
-
if (!outcome)
|
|
2519
|
-
return false;
|
|
2520
|
-
let claimed = false;
|
|
2521
|
-
await failClosedOnUnreadableReplay(step.runId, step.id, async () => {
|
|
2522
|
-
const run = await getWorkflowRun2(step.runId);
|
|
2523
|
-
if (!run || run.status !== "waiting" && run.status !== "running")
|
|
2524
|
-
return;
|
|
2525
|
-
const workflow = await getWorkflow2(run.workflowId);
|
|
2526
|
-
if (!workflow)
|
|
2527
|
-
return;
|
|
2528
|
-
if ("error" in outcome) {
|
|
2529
|
-
claimed = await failStepAndRunIfWaiting(step.id, run.id, outcome.error);
|
|
2530
|
-
return;
|
|
2531
|
-
}
|
|
2532
|
-
const ctx = run.context ?? {};
|
|
2533
|
-
const routing = await checkpointPortStepAndResolveSuccessors(workflow.definition, run.id, step.id, step.nodeId, outcome.output, "success", ctx);
|
|
2534
|
-
if (!routing.claimed)
|
|
2535
|
-
return;
|
|
2536
|
-
claimed = true;
|
|
2537
|
-
if (routing.successors.length > 0) {
|
|
2538
|
-
const secretKeys = getSecretInputKeys(workflow.input);
|
|
2539
|
-
await walkGraph(workflow.definition, run.id, ctx, routing.successors, registry, workflow.id, secretKeys);
|
|
2540
|
-
} else {
|
|
2541
|
-
await finalizeOrWait2(run.id);
|
|
2542
|
-
}
|
|
2543
|
-
});
|
|
2544
|
-
return claimed;
|
|
2545
|
-
}
|
|
2546
|
-
var FIRED_PAYLOAD_BYTE_CAP = 64 * 1024;
|
|
2547
|
-
function capPayload(payload) {
|
|
2548
|
-
if (payload === undefined || payload === null)
|
|
2549
|
-
return payload;
|
|
2550
|
-
let encoded;
|
|
2551
|
-
try {
|
|
2552
|
-
encoded = JSON.stringify(payload);
|
|
2553
|
-
} catch {
|
|
2554
|
-
return { truncated: true, reason: "non-serializable" };
|
|
2555
|
-
}
|
|
2556
|
-
if (encoded.length <= FIRED_PAYLOAD_BYTE_CAP) {
|
|
2557
|
-
return payload;
|
|
2558
|
-
}
|
|
2559
|
-
const summary = encoded.slice(0, 1024);
|
|
2560
|
-
return {
|
|
2561
|
-
truncated: true,
|
|
2562
|
-
originalSize: encoded.length,
|
|
2563
|
-
summary
|
|
2564
|
-
};
|
|
2565
|
-
}
|
|
2566
|
-
var waitsByEvent = new Map;
|
|
2567
|
-
var listenersByEvent = new Map;
|
|
2568
|
-
var busRegistry = null;
|
|
2569
|
-
async function initWaitBusSubscriptions2(registry) {
|
|
2570
|
-
busRegistry = registry;
|
|
2571
|
-
const pendingNames = await collectPendingEventNames();
|
|
2572
|
-
for (const name of pendingNames) {
|
|
2573
|
-
const pending = await getPendingWaitsByEvent2(name);
|
|
2574
|
-
for (const w of pending) {
|
|
2575
|
-
registerWait(w.id, name);
|
|
2576
|
-
}
|
|
2577
|
-
}
|
|
2578
|
-
}
|
|
2579
|
-
async function collectPendingEventNames() {
|
|
2580
|
-
return new Set(await getPendingEventWaitNames2());
|
|
2581
|
-
}
|
|
2582
|
-
function subscribeWaitToBus2(waitId, eventName) {
|
|
2583
|
-
registerWait(waitId, eventName);
|
|
2584
|
-
}
|
|
2585
|
-
function registerWait(waitId, eventName) {
|
|
2586
|
-
let set = waitsByEvent.get(eventName);
|
|
2587
|
-
if (!set) {
|
|
2588
|
-
set = new Set;
|
|
2589
|
-
waitsByEvent.set(eventName, set);
|
|
2590
|
-
}
|
|
2591
|
-
set.add(waitId);
|
|
2592
|
-
if (!listenersByEvent.has(eventName)) {
|
|
2593
|
-
const listener = (data) => {
|
|
2594
|
-
processBusEvent(eventName, data).catch((err) => {
|
|
2595
|
-
console.error(`[workflows] Wait bus listener failed for event=${eventName}:`, scrubSecrets(err instanceof Error ? err.message : String(err)));
|
|
2596
|
-
});
|
|
2597
|
-
};
|
|
2598
|
-
listenersByEvent.set(eventName, listener);
|
|
2599
|
-
workflowEventBus2.on(eventName, listener);
|
|
2600
|
-
}
|
|
2601
|
-
}
|
|
2602
|
-
function pruneWaitFromBus(waitId, eventName) {
|
|
2603
|
-
const set = waitsByEvent.get(eventName);
|
|
2604
|
-
if (!set)
|
|
2605
|
-
return;
|
|
2606
|
-
set.delete(waitId);
|
|
2607
|
-
if (set.size === 0) {
|
|
2608
|
-
waitsByEvent.delete(eventName);
|
|
2609
|
-
const listener = listenersByEvent.get(eventName);
|
|
2610
|
-
if (listener) {
|
|
2611
|
-
workflowEventBus2.off(eventName, listener);
|
|
2612
|
-
listenersByEvent.delete(eventName);
|
|
2613
|
-
}
|
|
2614
|
-
}
|
|
2615
|
-
}
|
|
2616
|
-
async function processBusEvent(eventName, payload) {
|
|
2617
|
-
const set = waitsByEvent.get(eventName);
|
|
2618
|
-
if (!set || set.size === 0)
|
|
2619
|
-
return;
|
|
2620
|
-
if (!busRegistry)
|
|
2621
|
-
return;
|
|
2622
|
-
const waitIds = [...set];
|
|
2623
|
-
for (const waitId of waitIds) {
|
|
2624
|
-
try {
|
|
2625
|
-
const row = await getWaitStateById2(waitId);
|
|
2626
|
-
if (!row || row.status !== "pending") {
|
|
2627
|
-
set.delete(waitId);
|
|
2628
|
-
continue;
|
|
2629
|
-
}
|
|
2630
|
-
if (row.eventScope === "run") {
|
|
2631
|
-
if (!isPayloadInRun(payload, row.workflowRunId))
|
|
2632
|
-
continue;
|
|
2633
|
-
}
|
|
2634
|
-
const ok = await matchesFilter(payload, row.eventFilter ?? undefined);
|
|
2635
|
-
if (!ok)
|
|
2636
|
-
continue;
|
|
2637
|
-
await resumeWaitState2(waitId, "fired", payload, busRegistry);
|
|
2638
|
-
} catch (err) {
|
|
2639
|
-
console.error(`[workflows] Wait bus listener failed for wait=${waitId} event=${eventName}:`, err);
|
|
2640
|
-
}
|
|
2641
|
-
}
|
|
2642
|
-
if (set.size === 0) {
|
|
2643
|
-
waitsByEvent.delete(eventName);
|
|
2644
|
-
const listener = listenersByEvent.get(eventName);
|
|
2645
|
-
if (listener) {
|
|
2646
|
-
workflowEventBus2.off(eventName, listener);
|
|
2647
|
-
listenersByEvent.delete(eventName);
|
|
2648
|
-
}
|
|
2649
|
-
}
|
|
2650
|
-
}
|
|
2651
|
-
function isPayloadInRun(payload, runId) {
|
|
2652
|
-
if (typeof payload !== "object" || payload === null)
|
|
2653
|
-
return false;
|
|
2654
|
-
const rec = payload;
|
|
2655
|
-
return rec._runId === runId || rec.workflowRunId === runId;
|
|
2656
|
-
}
|
|
2657
|
-
export { notifyAutomationPreflightFailure, automationIntegrationFixUrl, isNeedsSetupFailure, preflightAutomation, renderAutomationTokens, getAutomationSetupStates, schedulePreflightInput, listEnabledAutomationPreflightInputs, recordSchedulePreflightFailure, checkpointStep, checkpointStepFailure, checkpointStepWaiting, FOREACH_TERMINAL_STEP_STATUSES, parseSyntheticNodeId, buildForeachAggregate, loadCompletedStepRouting, resolveInputValue, getSecretInputKeys, validateJsonSchema, findJsonSchemaShapeErrors, getMaxWorkflowStepsPerRun, workflowSaveWarnings, withSaveWarnings, runStepValidation, shapeApprovalResolution, FAILED_TASK_OUTPUT_PREFIX, SubWorkflowExecutor, WaitExecutor, failStepAndRunIfWaiting, checkpointPortStepAndResolveSuccessors, scheduleTaskStepRetry, completeTaskStepAndResolveSuccessors, setupWorkflowResumeListener2, finalizeOrWait2, retryFailedRun2, cancelWorkflowRunRows2, cancelWorkflowRun2, resumeWaitState2, recoverResolvedWaitState2, resumeFromChildRun2, initWaitBusSubscriptions2, subscribeWaitToBus2, TriggerSchemaError, startWorkflowExecution2, failRunOnUnreadableReplay, isWorkflowRunActive, holdWorkflowRun, walkGraph, rehydrateCompletedStepOutputs, findReadyNodes, buildNodeInterpolationCtx, interpolateNodeConfig, scriptBodyInterpolationError };
|