@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
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { WorkflowDefinition, WorkflowNode } from "../types";
|
|
2
|
+
import type { ExecutorRegistry } from "./executors/registry";
|
|
3
|
+
import { resolveForeachParent } from "./foreach-join";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Executor types a `foreach` body must not use, with the reason. Every other
|
|
7
|
+
* registered executor is a valid body.
|
|
8
|
+
*/
|
|
9
|
+
export const FOREACH_BODY_REJECTED: Readonly<Record<string, string>> = {
|
|
10
|
+
foreach: "nested fan-out is not supported: synthetic ids and the join resolve one level only",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Built-in executors accepted as a `foreach` body, by how one item completes:
|
|
15
|
+
* `inline` items are run and settled by the foreach driver; `parks` items wait
|
|
16
|
+
* on their own resume path (task, child run, approval, wait), which settles
|
|
17
|
+
* the item through `settleForeachChild`. A `system-one-decision` without
|
|
18
|
+
* `humanReview` returns inline.
|
|
19
|
+
*
|
|
20
|
+
* Every built-in executor must appear here or in FOREACH_BODY_REJECTED; the
|
|
21
|
+
* registry exhaustiveness test enforces that a new executor gets a decision.
|
|
22
|
+
*/
|
|
23
|
+
export const FOREACH_BODY_ACCEPTED: Readonly<Record<string, "inline" | "parks">> = {
|
|
24
|
+
script: "inline",
|
|
25
|
+
"swarm-script": "inline",
|
|
26
|
+
"raw-llm": "inline",
|
|
27
|
+
notify: "inline",
|
|
28
|
+
"property-match": "inline",
|
|
29
|
+
"code-match": "inline",
|
|
30
|
+
validate: "inline",
|
|
31
|
+
vcs: "inline",
|
|
32
|
+
"system-one-extraction": "inline",
|
|
33
|
+
"agent-task": "parks",
|
|
34
|
+
"sub-workflow": "parks",
|
|
35
|
+
"human-in-the-loop": "parks",
|
|
36
|
+
wait: "parks",
|
|
37
|
+
"system-one-decision": "parks",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* True when `type` may be a foreach body: not rejected, and registered (or a
|
|
42
|
+
* known built-in when no registry is passed).
|
|
43
|
+
*/
|
|
44
|
+
export function isForeachBodyType(type: string, registry?: ExecutorRegistry): boolean {
|
|
45
|
+
if (Object.hasOwn(FOREACH_BODY_REJECTED, type)) return false;
|
|
46
|
+
return registry ? registry.has(type) : Object.hasOwn(FOREACH_BODY_ACCEPTED, type);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The validation error for a foreach body type, or null when it is allowed.
|
|
51
|
+
* Without a registry only the rejection list applies, so a custom executor
|
|
52
|
+
* registered at run time is not refused by a registry-less validation pass.
|
|
53
|
+
*/
|
|
54
|
+
export function foreachBodyTypeError(
|
|
55
|
+
nodeId: string,
|
|
56
|
+
type: string,
|
|
57
|
+
registry?: ExecutorRegistry,
|
|
58
|
+
): string | null {
|
|
59
|
+
const reason = FOREACH_BODY_REJECTED[type];
|
|
60
|
+
if (reason !== undefined) {
|
|
61
|
+
return `Node "${nodeId}" foreach config.body.type "${type}" is not supported: ${reason} (a ${type} body inside foreach "${nodeId}" is rejected)`;
|
|
62
|
+
}
|
|
63
|
+
if (registry && !registry.has(type)) {
|
|
64
|
+
return `Node "${nodeId}" foreach config.body.type "${type}" is not a registered executor`;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The definition node a step row executes. A real node wins. A synthetic
|
|
71
|
+
* foreach child id (`<foreachId>#<itemKey>`) resolves to a node synthesized
|
|
72
|
+
* from the parent's body: the child's id, the body's type and config, and the
|
|
73
|
+
* parent's declared `inputs` (the body sees the parent's input context, and the
|
|
74
|
+
* script-body allowlist classifies those aliases by their source).
|
|
75
|
+
*/
|
|
76
|
+
export function resolveStepNode(
|
|
77
|
+
def: Pick<WorkflowDefinition, "nodes">,
|
|
78
|
+
nodeId: string,
|
|
79
|
+
): WorkflowNode | null {
|
|
80
|
+
const node = def.nodes.find((candidate) => candidate.id === nodeId);
|
|
81
|
+
if (node) return node;
|
|
82
|
+
const parent = resolveForeachParent(def, nodeId);
|
|
83
|
+
if (!parent) return null;
|
|
84
|
+
const body = parent.config.body;
|
|
85
|
+
if (!isRecord(body) || typeof body.type !== "string") return null;
|
|
86
|
+
// `item` and `index` are bound per item, so a parent alias with either name
|
|
87
|
+
// is dropped: it would otherwise classify item data by the alias's source
|
|
88
|
+
// and let it past the script-body allowlist.
|
|
89
|
+
const inputs = parent.inputs
|
|
90
|
+
? Object.fromEntries(
|
|
91
|
+
Object.entries(parent.inputs).filter(([alias]) => !FOREACH_ITEM_BINDINGS.has(alias)),
|
|
92
|
+
)
|
|
93
|
+
: undefined;
|
|
94
|
+
return {
|
|
95
|
+
id: nodeId,
|
|
96
|
+
type: body.type,
|
|
97
|
+
config: isRecord(body.config) ? body.config : {},
|
|
98
|
+
...(inputs ? { inputs } : {}),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const FOREACH_ITEM_BINDINGS = new Set(["item", "index"]);
|
|
103
|
+
|
|
104
|
+
export const FOREACH_FAILURE_POLICIES = ["fail-fast", "collect-outcomes"] as const;
|
|
105
|
+
export type ForeachFailurePolicy = (typeof FOREACH_FAILURE_POLICIES)[number];
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* How a failed or cancelled item affects its fan-out. `config.failurePolicy`
|
|
109
|
+
* wins; otherwise it follows the workflow's `onNodeFailure`, so a definition
|
|
110
|
+
* without the field behaves as before: `"continue"` collects outcomes, the
|
|
111
|
+
* default `"fail"` is fail-fast.
|
|
112
|
+
*/
|
|
113
|
+
export function effectiveFailurePolicy(
|
|
114
|
+
node: WorkflowNode,
|
|
115
|
+
def: Pick<WorkflowDefinition, "onNodeFailure">,
|
|
116
|
+
): ForeachFailurePolicy {
|
|
117
|
+
const configured = node.config.failurePolicy;
|
|
118
|
+
if (configured === "fail-fast" || configured === "collect-outcomes") return configured;
|
|
119
|
+
return def.onNodeFailure === "continue" ? "collect-outcomes" : "fail-fast";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** How many items of a fan-out may be in flight at once. Unbounded by default. */
|
|
123
|
+
export function foreachConcurrencyCap(node: WorkflowNode): number {
|
|
124
|
+
const cap = node.config.concurrency;
|
|
125
|
+
return typeof cap === "number" && Number.isInteger(cap) && cap >= 1
|
|
126
|
+
? cap
|
|
127
|
+
: Number.POSITIVE_INFINITY;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The failure mode a foreach child's task failure takes: the fan-out's
|
|
132
|
+
* effective policy for a synthetic child, the workflow's `onNodeFailure`
|
|
133
|
+
* otherwise.
|
|
134
|
+
*/
|
|
135
|
+
export function stepFailureMode(
|
|
136
|
+
def: Pick<WorkflowDefinition, "nodes" | "onNodeFailure">,
|
|
137
|
+
nodeId: string,
|
|
138
|
+
): "fail" | "continue" {
|
|
139
|
+
const parent = resolveForeachParent(def, nodeId);
|
|
140
|
+
if (parent) return effectiveFailurePolicy(parent, def) === "fail-fast" ? "fail" : "continue";
|
|
141
|
+
return def.onNodeFailure ?? "fail";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
145
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
146
|
+
}
|
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import { renderAutomationTokens } from "../be/automation-preflight";
|
|
2
|
+
import {
|
|
3
|
+
detachTaskFromWorkflowRunStep,
|
|
4
|
+
getDbClient,
|
|
5
|
+
getLatestStepForNode,
|
|
6
|
+
getTaskByWorkflowRunStepId,
|
|
7
|
+
getWorkflow,
|
|
8
|
+
getWorkflowRun,
|
|
9
|
+
getWorkflowRunStep,
|
|
10
|
+
getWorkflowRunStepsByRunId,
|
|
11
|
+
updateWorkflowRun,
|
|
12
|
+
updateWorkflowRunStep,
|
|
13
|
+
} from "../be/db";
|
|
14
|
+
import type {
|
|
15
|
+
Workflow,
|
|
16
|
+
WorkflowDefinition,
|
|
17
|
+
WorkflowNode,
|
|
18
|
+
WorkflowRun,
|
|
19
|
+
WorkflowRunStep,
|
|
20
|
+
} from "../types";
|
|
21
|
+
import { checkpointStepWaiting } from "./checkpoint";
|
|
22
|
+
import { FOREACH_ITEMS_WAIT_FOR } from "./constants";
|
|
23
|
+
import {
|
|
24
|
+
hasRunningStep,
|
|
25
|
+
holdWorkflowRun,
|
|
26
|
+
interpolateNodeConfig,
|
|
27
|
+
scriptBodyInterpolationError,
|
|
28
|
+
walkGraph,
|
|
29
|
+
} from "./engine";
|
|
30
|
+
import type { AsyncExecutorResult } from "./executors/base";
|
|
31
|
+
import { ForeachConfigSchema } from "./executors/foreach";
|
|
32
|
+
import type { ExecutorRegistry } from "./executors/registry";
|
|
33
|
+
import { systemOneUnresolvedError } from "./executors/system-one-decision";
|
|
34
|
+
import {
|
|
35
|
+
effectiveFailurePolicy,
|
|
36
|
+
foreachConcurrencyCap,
|
|
37
|
+
isForeachBodyType,
|
|
38
|
+
resolveStepNode,
|
|
39
|
+
} from "./foreach-body";
|
|
40
|
+
import {
|
|
41
|
+
cancelQueuedForeachChildren,
|
|
42
|
+
FOREACH_TERMINAL_STEP_STATUSES,
|
|
43
|
+
type ForeachChildOutcome,
|
|
44
|
+
latestForeachChildren,
|
|
45
|
+
settleForeachChild,
|
|
46
|
+
} from "./foreach-join";
|
|
47
|
+
import { getSecretInputKeys } from "./input";
|
|
48
|
+
import { resolveNodeTimeoutMs } from "./node-timeout";
|
|
49
|
+
import { rebuildNodeExecutionInput } from "./step-input";
|
|
50
|
+
|
|
51
|
+
export { FOREACH_ITEMS_WAIT_FOR };
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Child step ids a drive in this process is starting right now. A `running`
|
|
55
|
+
* child outside this set has no owner: a crash (or a lost drive) left it
|
|
56
|
+
* between its claim and its settlement, and the next drive restarts it.
|
|
57
|
+
*/
|
|
58
|
+
const ownedChildren = new Set<string>();
|
|
59
|
+
|
|
60
|
+
/** Fire-and-forget drives, tracked so tests and shutdown can drain them. */
|
|
61
|
+
const activeDrives = new Set<Promise<void>>();
|
|
62
|
+
|
|
63
|
+
export interface ForeachDriveResult {
|
|
64
|
+
/** True when a settlement in this drive closed the join. */
|
|
65
|
+
joined: boolean;
|
|
66
|
+
/** The parent's successors to walk, when `joined`. */
|
|
67
|
+
successors: WorkflowNode[];
|
|
68
|
+
/** How many children this drive claimed (queued or orphaned). */
|
|
69
|
+
claimed: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const NOT_JOINED: ForeachDriveResult = { joined: false, successors: [], claimed: 0 };
|
|
73
|
+
|
|
74
|
+
export function isForeachChildOwned(stepId: string): boolean {
|
|
75
|
+
return ownedChildren.has(stepId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Drop in-process child ownership, as a restart would. Tests only. */
|
|
79
|
+
export function _resetForeachOwnershipForTests(): void {
|
|
80
|
+
ownedChildren.clear();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Park a foreach parent whose executor materialized its children, then drive
|
|
85
|
+
* them. The run stays `running` while the drive starts the children; when the
|
|
86
|
+
* join stays open it parks as `waiting` like any async step. Shared by
|
|
87
|
+
* executeStep and the retry poller.
|
|
88
|
+
*/
|
|
89
|
+
export async function parkAndDriveForeach(input: {
|
|
90
|
+
runId: string;
|
|
91
|
+
stepId: string;
|
|
92
|
+
parentNodeId: string;
|
|
93
|
+
ctx: Record<string, unknown>;
|
|
94
|
+
registry: ExecutorRegistry;
|
|
95
|
+
}): Promise<ForeachDriveResult & { parked: boolean }> {
|
|
96
|
+
// A re-walk of an open fan-out (a run retry, recovery) reuses a parent step
|
|
97
|
+
// that is already parked: drive it as it is.
|
|
98
|
+
const parked =
|
|
99
|
+
(await checkpointStepWaiting(input.runId, input.stepId, input.ctx, {
|
|
100
|
+
keepRunStatus: true,
|
|
101
|
+
})) || (await getWorkflowRunStep(input.stepId))?.status === "waiting";
|
|
102
|
+
if (!parked) return { ...NOT_JOINED, parked: false };
|
|
103
|
+
const drive = await driveForeach(input.runId, input.parentNodeId, input.registry, {
|
|
104
|
+
ctx: input.ctx,
|
|
105
|
+
});
|
|
106
|
+
if (!drive.joined) await parkRunIfRunning(input.runId);
|
|
107
|
+
return { ...drive, parked: true };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Claim and start the children of foreach `parentNodeId` that are free to
|
|
112
|
+
* run: queued (`pending`) children up to the cap, plus orphaned `running`
|
|
113
|
+
* children no drive owns. Instant bodies run and settle here, each against its
|
|
114
|
+
* own watchdog budget. Async bodies park, and their own resume path settles
|
|
115
|
+
* them through `settleForeachChild`.
|
|
116
|
+
*
|
|
117
|
+
* Idempotent: the claim flips each child out of `pending` in a `BEGIN
|
|
118
|
+
* IMMEDIATE` transaction, and every async executor dedupes on the child step
|
|
119
|
+
* id, so a repeated drive never duplicates a task, child run or approval.
|
|
120
|
+
*/
|
|
121
|
+
export async function driveForeach(
|
|
122
|
+
runId: string,
|
|
123
|
+
parentNodeId: string,
|
|
124
|
+
registry: ExecutorRegistry,
|
|
125
|
+
options: { ctx?: Record<string, unknown> } = {},
|
|
126
|
+
): Promise<ForeachDriveResult> {
|
|
127
|
+
const run = await getWorkflowRun(runId);
|
|
128
|
+
if (!run || (run.status !== "running" && run.status !== "waiting")) return NOT_JOINED;
|
|
129
|
+
const workflow = await getRenderedWorkflow(run.workflowId);
|
|
130
|
+
if (!workflow) return NOT_JOINED;
|
|
131
|
+
const def = workflow.definition;
|
|
132
|
+
const parentNode = def.nodes.find((node) => node.id === parentNodeId);
|
|
133
|
+
if (!parentNode || parentNode.type !== "foreach") return NOT_JOINED;
|
|
134
|
+
const parentStep = await getLatestStepForNode(runId, parentNodeId);
|
|
135
|
+
if (!parentStep || FOREACH_TERMINAL_STEP_STATUSES.has(parentStep.status)) return NOT_JOINED;
|
|
136
|
+
|
|
137
|
+
const body = parentNode.config.body as { type?: unknown } | undefined;
|
|
138
|
+
const bodyType = typeof body?.type === "string" ? body.type : "";
|
|
139
|
+
// Rebuilt once, on the first claim: most drives (a settlement with nothing
|
|
140
|
+
// queued behind it) claim nothing and never pay for the context rebuild.
|
|
141
|
+
let parentInput: Awaited<ReturnType<typeof loadParentInput>> | undefined;
|
|
142
|
+
let claimedTotal = 0;
|
|
143
|
+
const notJoined = (): ForeachDriveResult => ({ ...NOT_JOINED, claimed: claimedTotal });
|
|
144
|
+
|
|
145
|
+
for (;;) {
|
|
146
|
+
const current = await getWorkflowRun(runId);
|
|
147
|
+
if (!current || (current.status !== "running" && current.status !== "waiting")) {
|
|
148
|
+
return notJoined();
|
|
149
|
+
}
|
|
150
|
+
const claimed = await claimChildren(def, runId, parentNode);
|
|
151
|
+
if (claimed.length === 0) return notJoined();
|
|
152
|
+
claimedTotal += claimed.length;
|
|
153
|
+
const started = new Set<string>();
|
|
154
|
+
let failedFast: string | undefined;
|
|
155
|
+
try {
|
|
156
|
+
parentInput ??= await loadParentInput(def, run, parentNode, registry, workflow.id, options);
|
|
157
|
+
const { ctx, items, itemKeyField, meta } = parentInput;
|
|
158
|
+
for (const child of claimed) {
|
|
159
|
+
// An earlier body in this batch can run for a long time. A cancel (of
|
|
160
|
+
// the run or this child) that landed meanwhile must stop the rest of
|
|
161
|
+
// the batch before it reaches an executor. A fail-fast failure of a
|
|
162
|
+
// sibling elsewhere ends the run too; `finally` cancels the rest.
|
|
163
|
+
const liveness = await childLiveness(runId, child.id);
|
|
164
|
+
if (liveness === "run-ended") return notJoined();
|
|
165
|
+
if (liveness === "child-ended") continue;
|
|
166
|
+
started.add(child.id);
|
|
167
|
+
const outcome = await startChild({
|
|
168
|
+
def,
|
|
169
|
+
child,
|
|
170
|
+
registry,
|
|
171
|
+
ctx,
|
|
172
|
+
meta,
|
|
173
|
+
bodyType,
|
|
174
|
+
item: items.find((candidate) => candidate[itemKeyField] === childItemKey(child)),
|
|
175
|
+
});
|
|
176
|
+
if (!outcome) continue;
|
|
177
|
+
const settled = await settleForeachChild(def, runId, child.id, outcome, ctx, {
|
|
178
|
+
from: ["running"],
|
|
179
|
+
});
|
|
180
|
+
ownedChildren.delete(child.id);
|
|
181
|
+
if (settled.joined) {
|
|
182
|
+
return { joined: true, successors: settled.successors, claimed: claimedTotal };
|
|
183
|
+
}
|
|
184
|
+
if (settled.runFailed) {
|
|
185
|
+
failedFast = child.nodeId;
|
|
186
|
+
return notJoined();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} finally {
|
|
190
|
+
// A child claimed but never started (an earlier sibling failed the run,
|
|
191
|
+
// or the input rebuild threw) goes back to the queue, so a run retry
|
|
192
|
+
// starts it. After a fail-fast failure it is cancelled with the rest of
|
|
193
|
+
// the queue instead: it never started.
|
|
194
|
+
for (const child of claimed) {
|
|
195
|
+
ownedChildren.delete(child.id);
|
|
196
|
+
if (!started.has(child.id)) await requeueChild(child.id);
|
|
197
|
+
}
|
|
198
|
+
if (
|
|
199
|
+
effectiveFailurePolicy(parentNode, def) === "fail-fast" &&
|
|
200
|
+
(failedFast || (await getWorkflowRun(runId))?.status === "failed")
|
|
201
|
+
) {
|
|
202
|
+
await cancelQueuedForeachChildren(def, runId, parentNodeId, failedFast);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The parent's execution input, rebuilt from persisted state the way the walk
|
|
210
|
+
* built it. Items are looked up by key in the re-interpolated `over`, never
|
|
211
|
+
* persisted on the child row, so resolved secrets stay out of step rows.
|
|
212
|
+
*/
|
|
213
|
+
async function loadParentInput(
|
|
214
|
+
def: WorkflowDefinition,
|
|
215
|
+
run: WorkflowRun,
|
|
216
|
+
parentNode: WorkflowNode,
|
|
217
|
+
registry: ExecutorRegistry,
|
|
218
|
+
workflowId: string,
|
|
219
|
+
options: { ctx?: Record<string, unknown> },
|
|
220
|
+
) {
|
|
221
|
+
const input = await rebuildNodeExecutionInput(def, run, parentNode, registry, {
|
|
222
|
+
ctx: options.ctx,
|
|
223
|
+
});
|
|
224
|
+
const parsed = ForeachConfigSchema.safeParse(input.config);
|
|
225
|
+
return {
|
|
226
|
+
ctx: input.ctx,
|
|
227
|
+
items: parsed.success ? parsed.data.over : [],
|
|
228
|
+
itemKeyField: parsed.success ? parsed.data.itemKey : "",
|
|
229
|
+
meta: {
|
|
230
|
+
runId: run.id,
|
|
231
|
+
workflowId,
|
|
232
|
+
dryRun: false,
|
|
233
|
+
requestedByUserId: run.createdBy,
|
|
234
|
+
inputCtx: input.inputCtx,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Start a drive without awaiting it. When it closes the join, it walks the
|
|
241
|
+
* parent's successors (or finalizes); otherwise it parks the run if nothing is
|
|
242
|
+
* left running. Errors are logged.
|
|
243
|
+
*/
|
|
244
|
+
export function startForeachDrive(
|
|
245
|
+
runId: string,
|
|
246
|
+
parentNodeId: string,
|
|
247
|
+
registry: ExecutorRegistry,
|
|
248
|
+
): void {
|
|
249
|
+
const drive = runForeachDrive(runId, parentNodeId, registry)
|
|
250
|
+
.then(() => undefined)
|
|
251
|
+
.catch((err) => {
|
|
252
|
+
console.error(`[workflows] foreach drive of ${parentNodeId} in run ${runId} failed:`, err);
|
|
253
|
+
})
|
|
254
|
+
.finally(() => {
|
|
255
|
+
activeDrives.delete(drive);
|
|
256
|
+
});
|
|
257
|
+
activeDrives.add(drive);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Drive a fan-out outside a live walk and route what it closes. Holds the run
|
|
262
|
+
* for the duration so the running-run recovery leaves it alone. Returns true
|
|
263
|
+
* when the drive started or settled at least one child.
|
|
264
|
+
*/
|
|
265
|
+
export async function runForeachDrive(
|
|
266
|
+
runId: string,
|
|
267
|
+
parentNodeId: string,
|
|
268
|
+
registry: ExecutorRegistry,
|
|
269
|
+
): Promise<ForeachDriveResult> {
|
|
270
|
+
const release = holdWorkflowRun(runId);
|
|
271
|
+
try {
|
|
272
|
+
const drive = await driveForeach(runId, parentNodeId, registry);
|
|
273
|
+
if (!drive.joined) {
|
|
274
|
+
await parkRunIfRunning(runId);
|
|
275
|
+
return drive;
|
|
276
|
+
}
|
|
277
|
+
const run = await getWorkflowRun(runId);
|
|
278
|
+
const workflow = run ? await getRenderedWorkflow(run.workflowId) : null;
|
|
279
|
+
if (!run || !workflow) return drive;
|
|
280
|
+
const ctx = (run.context ?? {}) as Record<string, unknown>;
|
|
281
|
+
if (drive.successors.length > 0) {
|
|
282
|
+
await walkGraph(
|
|
283
|
+
workflow.definition,
|
|
284
|
+
runId,
|
|
285
|
+
ctx,
|
|
286
|
+
drive.successors,
|
|
287
|
+
registry,
|
|
288
|
+
workflow.id,
|
|
289
|
+
getSecretInputKeys(workflow.input),
|
|
290
|
+
);
|
|
291
|
+
} else {
|
|
292
|
+
const { finalizeOrWait } = await import("./resume");
|
|
293
|
+
await finalizeOrWait(runId);
|
|
294
|
+
}
|
|
295
|
+
return drive;
|
|
296
|
+
} finally {
|
|
297
|
+
release();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Wait for every fire-and-forget drive, including drives they start. */
|
|
302
|
+
export async function drainForeachDrives(): Promise<void> {
|
|
303
|
+
while (activeDrives.size > 0) {
|
|
304
|
+
await Promise.allSettled([...activeDrives]);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* The run's workflow with its install params rendered in, as
|
|
310
|
+
* startWorkflowExecution rendered it before the walk. The driver and its
|
|
311
|
+
* post-join walk reload the definition, so without this a `{{PARAM}}` token in
|
|
312
|
+
* the body config or in `over` would reach the per-item pass unresolved.
|
|
313
|
+
*/
|
|
314
|
+
async function getRenderedWorkflow(workflowId: string): Promise<Workflow | null> {
|
|
315
|
+
const workflow = await getWorkflow(workflowId);
|
|
316
|
+
return workflow ? renderAutomationTokens(workflow, workflow.params ?? {}) : null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Whether a claimed child may still start: its run is live and its row is still claimed. */
|
|
320
|
+
async function childLiveness(
|
|
321
|
+
runId: string,
|
|
322
|
+
stepId: string,
|
|
323
|
+
): Promise<"live" | "run-ended" | "child-ended"> {
|
|
324
|
+
const run = await getWorkflowRun(runId);
|
|
325
|
+
if (!run || (run.status !== "running" && run.status !== "waiting")) return "run-ended";
|
|
326
|
+
const step = await getWorkflowRunStep(stepId);
|
|
327
|
+
return step?.status === "running" ? "live" : "child-ended";
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function requeueChild(stepId: string): Promise<void> {
|
|
331
|
+
await getDbClient().run(
|
|
332
|
+
"UPDATE workflow_run_steps SET status = 'pending' WHERE id = ? AND status = 'running'",
|
|
333
|
+
[stepId],
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Park a `running` run as `waiting` once nothing in it is executing and a step
|
|
339
|
+
* is waiting. Never completes the run: a drive that did not close the join
|
|
340
|
+
* must not finalize a run whose joiner is still walking the successors.
|
|
341
|
+
*/
|
|
342
|
+
async function parkRunIfRunning(runId: string): Promise<void> {
|
|
343
|
+
await getDbClient().transaction(async () => {
|
|
344
|
+
const run = await getWorkflowRun(runId);
|
|
345
|
+
if (run?.status !== "running") return;
|
|
346
|
+
const steps = await getWorkflowRunStepsByRunId(runId);
|
|
347
|
+
if (hasRunningStep(steps)) return;
|
|
348
|
+
if (!steps.some((step) => step.status === "waiting")) return;
|
|
349
|
+
await updateWorkflowRun(runId, { status: "waiting" });
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Flip `pending` children to `running`, in item order, while fewer than the
|
|
355
|
+
* fan-out's `concurrency` cap are in flight (`running` or `waiting`, latest
|
|
356
|
+
* attempt rows only), and collect unowned `running` ones. Ownership is registered before commit, so no other
|
|
357
|
+
* drive can see a claimed row without also seeing it owned.
|
|
358
|
+
*/
|
|
359
|
+
async function claimChildren(
|
|
360
|
+
def: WorkflowDefinition,
|
|
361
|
+
runId: string,
|
|
362
|
+
parentNode: WorkflowNode,
|
|
363
|
+
): Promise<WorkflowRunStep[]> {
|
|
364
|
+
const claimed: WorkflowRunStep[] = [];
|
|
365
|
+
try {
|
|
366
|
+
// The in-flight count and the claim share one BEGIN IMMEDIATE transaction,
|
|
367
|
+
// so concurrent drives (a settlement's drive and the recovery sweep)
|
|
368
|
+
// serialize here and never exceed the cap together.
|
|
369
|
+
await getDbClient().transaction(async () => {
|
|
370
|
+
const children = latestForeachChildren(
|
|
371
|
+
def,
|
|
372
|
+
parentNode.id,
|
|
373
|
+
await getWorkflowRunStepsByRunId(runId),
|
|
374
|
+
);
|
|
375
|
+
for (const child of children) {
|
|
376
|
+
if (child.status === "running" && !ownedChildren.has(child.id)) {
|
|
377
|
+
ownedChildren.add(child.id);
|
|
378
|
+
claimed.push(child);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const inFlight = children.filter(
|
|
382
|
+
(child) => child.status === "running" || child.status === "waiting",
|
|
383
|
+
).length;
|
|
384
|
+
let free = foreachConcurrencyCap(parentNode) - inFlight;
|
|
385
|
+
for (const child of children) {
|
|
386
|
+
if (free <= 0) break;
|
|
387
|
+
if (child.status !== "pending") continue;
|
|
388
|
+
const row = await getDbClient().get<{ id: string }>(
|
|
389
|
+
"UPDATE workflow_run_steps SET status = 'running' WHERE id = ? AND status = 'pending' RETURNING id",
|
|
390
|
+
[child.id],
|
|
391
|
+
);
|
|
392
|
+
if (!row) continue;
|
|
393
|
+
ownedChildren.add(child.id);
|
|
394
|
+
claimed.push({ ...child, status: "running" });
|
|
395
|
+
free--;
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
} catch (err) {
|
|
399
|
+
for (const child of claimed) ownedChildren.delete(child.id);
|
|
400
|
+
throw err;
|
|
401
|
+
}
|
|
402
|
+
return claimed;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Run one claimed child's body. Returns its terminal outcome, or null when it
|
|
407
|
+
* parked (or another handler moved it on).
|
|
408
|
+
*/
|
|
409
|
+
async function startChild(input: {
|
|
410
|
+
def: WorkflowDefinition;
|
|
411
|
+
child: WorkflowRunStep;
|
|
412
|
+
registry: ExecutorRegistry;
|
|
413
|
+
ctx: Record<string, unknown>;
|
|
414
|
+
meta: {
|
|
415
|
+
runId: string;
|
|
416
|
+
workflowId: string;
|
|
417
|
+
dryRun: boolean;
|
|
418
|
+
requestedByUserId?: string;
|
|
419
|
+
inputCtx: Record<string, unknown>;
|
|
420
|
+
};
|
|
421
|
+
bodyType: string;
|
|
422
|
+
item: Record<string, unknown> | undefined;
|
|
423
|
+
}): Promise<ForeachChildOutcome | null> {
|
|
424
|
+
const { def, child, registry, ctx, meta, bodyType, item } = input;
|
|
425
|
+
if (!item) {
|
|
426
|
+
return {
|
|
427
|
+
status: "failed",
|
|
428
|
+
error: `item "${childItemKey(child)}" is no longer present in over`,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
if (!isForeachBodyType(bodyType, registry)) {
|
|
432
|
+
return { status: "failed", error: `foreach body type "${bodyType}" cannot run` };
|
|
433
|
+
}
|
|
434
|
+
const bodyNode = resolveStepNode(def, child.nodeId);
|
|
435
|
+
if (!bodyNode) return { status: "failed", error: `foreach child ${child.nodeId} has no body` };
|
|
436
|
+
|
|
437
|
+
// The per-item pass interpolates against the parent's declared-inputs
|
|
438
|
+
// context plus item/index, NOT the full run context, so the body obeys the
|
|
439
|
+
// same explicit-dataflow boundary as a normal node. A script body keeps the
|
|
440
|
+
// script-source allowlist: `item` reaches it through `args`, never source.
|
|
441
|
+
const index = childIndex(child);
|
|
442
|
+
const bodyCtx = { ...meta.inputCtx, item, index };
|
|
443
|
+
const interpolation = interpolateNodeConfig(bodyNode, bodyCtx);
|
|
444
|
+
if (interpolation.scriptBodyUnresolved && interpolation.scriptBodyUnresolved.length > 0) {
|
|
445
|
+
return {
|
|
446
|
+
status: "failed",
|
|
447
|
+
error: scriptBodyInterpolationError(child.nodeId, interpolation.scriptBodyUnresolved),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
if (interpolation.strictUnresolved && interpolation.strictUnresolved.length > 0) {
|
|
451
|
+
return {
|
|
452
|
+
status: "failed",
|
|
453
|
+
error: systemOneUnresolvedError(child.nodeId, interpolation.strictUnresolved, bodyType),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
if (interpolation.unresolved.length > 0) {
|
|
457
|
+
console.warn(
|
|
458
|
+
`[workflow] Step ${child.nodeId}: unresolved interpolation tokens: ${interpolation.unresolved.join(", ")}`,
|
|
459
|
+
);
|
|
460
|
+
await updateWorkflowRunStep(child.id, {
|
|
461
|
+
diagnostics: JSON.stringify({ unresolvedTokens: interpolation.unresolved }),
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
const config = interpolation.value as Record<string, unknown>;
|
|
465
|
+
|
|
466
|
+
if (bodyType === "agent-task") {
|
|
467
|
+
// A retried item needs a fresh task. Detaching the stale terminal task keeps
|
|
468
|
+
// the executor's one-task-per-step de-duplication deterministic and scoped.
|
|
469
|
+
const linkedTask = await getTaskByWorkflowRunStepId(child.id);
|
|
470
|
+
if (linkedTask?.status === "failed" || linkedTask?.status === "cancelled") {
|
|
471
|
+
await detachTaskFromWorkflowRunStep(linkedTask.id);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const executor = registry.get(bodyType);
|
|
476
|
+
const timeoutMs = resolveNodeTimeoutMs({ type: bodyType, config });
|
|
477
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
478
|
+
let result: Awaited<ReturnType<typeof executor.run>>;
|
|
479
|
+
try {
|
|
480
|
+
result = await Promise.race([
|
|
481
|
+
executor.run({
|
|
482
|
+
config,
|
|
483
|
+
context: { ...ctx, ...bodyCtx },
|
|
484
|
+
meta: { ...meta, stepId: child.id, nodeId: child.nodeId },
|
|
485
|
+
}),
|
|
486
|
+
new Promise<never>((_, reject) => {
|
|
487
|
+
timer = setTimeout(
|
|
488
|
+
() => reject(new Error(`Step timed out after ${timeoutMs}ms`)),
|
|
489
|
+
timeoutMs,
|
|
490
|
+
);
|
|
491
|
+
}),
|
|
492
|
+
]);
|
|
493
|
+
} catch (err) {
|
|
494
|
+
return { status: "failed", error: err instanceof Error ? err.message : String(err) };
|
|
495
|
+
} finally {
|
|
496
|
+
if (timer) clearTimeout(timer);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (result.status === "failed") {
|
|
500
|
+
return {
|
|
501
|
+
status: "failed",
|
|
502
|
+
error: result.error || "Executor returned failed status",
|
|
503
|
+
...(result.output !== undefined ? { output: result.output } : {}),
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if ("async" in result && (result as AsyncExecutorResult).async) {
|
|
508
|
+
const parked = await getDbClient().get<{ id: string }>(
|
|
509
|
+
"UPDATE workflow_run_steps SET status = 'waiting' WHERE id = ? AND status = 'running' RETURNING id",
|
|
510
|
+
[child.id],
|
|
511
|
+
);
|
|
512
|
+
// A child run that finished before the item parked had its event dropped.
|
|
513
|
+
if (parked && (result as AsyncExecutorResult).waitFor === "workflow.child.finished") {
|
|
514
|
+
ownedChildren.delete(child.id);
|
|
515
|
+
const { resumeFromChildRun } = await import("./resume");
|
|
516
|
+
await resumeFromChildRun(child.id, registry);
|
|
517
|
+
}
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
status: "completed",
|
|
523
|
+
output: bodyType === "agent-task" ? normalizeExistingTaskOutput(result.output) : result.output,
|
|
524
|
+
...(result.nextPort ? { nextPort: result.nextPort } : {}),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function childItemKey(child: WorkflowRunStep): string | undefined {
|
|
529
|
+
const input = child.input as Record<string, unknown> | null | undefined;
|
|
530
|
+
return typeof input?.itemKey === "string" ? input.itemKey : undefined;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function childIndex(child: WorkflowRunStep): number {
|
|
534
|
+
const input = child.input as Record<string, unknown> | null | undefined;
|
|
535
|
+
return typeof input?.index === "number" ? input.index : 0;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** An agent-task body that reconnected to an already-completed task. */
|
|
539
|
+
function normalizeExistingTaskOutput(output: unknown): unknown {
|
|
540
|
+
if (typeof output !== "object" || output === null) return output;
|
|
541
|
+
const record = output as Record<string, unknown>;
|
|
542
|
+
if (typeof record.taskOutput !== "string") return output;
|
|
543
|
+
try {
|
|
544
|
+
const parsed = JSON.parse(record.taskOutput);
|
|
545
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
546
|
+
return { ...record, taskOutput: parsed };
|
|
547
|
+
}
|
|
548
|
+
} catch {
|
|
549
|
+
// Non-structured task output remains a string.
|
|
550
|
+
}
|
|
551
|
+
return output;
|
|
552
|
+
}
|