@desplega.ai/agent-swarm 2.5.4 → 2.7.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-ek38cwsv.js → acp-adapter-ma1js5n7.js} +9 -9
- package/dist/{actions-92mepq03.js → actions-1vhmtvg9.js} +21 -21
- package/dist/{amp-adapter-9gaxg015.js → amp-adapter-sxqqvmdk.js} +4 -4
- package/dist/{app-ctn8gsgx.js → app-mwx1d5a9.js} +3 -3
- package/dist/{assistant-zyr5kz47.js → assistant-yx1p6g82.js} +30 -26
- package/dist/{boot-reembed-w75maga2.js → boot-reembed-5h37a2c2.js} +1 -1
- package/dist/{boot-reembed-dwzfttdr.js → boot-reembed-7h78vmvv.js} +2 -1
- package/dist/{boot-scrub-sweep-e9kq43kf.js → boot-scrub-sweep-8mawd4gw.js} +4 -4
- package/dist/{boot-seeds-x28v3fhb.js → boot-seeds-ca84dhnn.js} +4 -4
- package/dist/{claude-adapter-f0afqnne.js → claude-adapter-wqt4hfkq.js} +9 -9
- package/dist/{claude-managed-adapter-x81a7r65.js → claude-managed-adapter-6fjqtr2z.js} +2 -2
- package/dist/{claude-managed-setup-bqzwjd9x.js → claude-managed-setup-rqv98sea.js} +10 -3
- package/dist/{claude-sdk-session-7b7whkaq.js → claude-sdk-session-f29f3b2x.js} +11 -11
- package/dist/{cli-sxgtf5fw.js → cli-07q03xm2.js} +1 -1
- package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
- package/dist/{cli-g9vd9ece.js → cli-1hhbzkbb.js} +14 -16
- package/dist/{cli-qqzmjxkf.js → cli-1m9x8dr5.js} +11 -6
- package/dist/{cli-jbnn4vxb.js → cli-2pvzmas1.js} +3 -3
- package/dist/{cli-x77gen2p.js → cli-2y7v133v.js} +7 -7
- package/dist/{cli-dew6x255.js → cli-39aet655.js} +2 -2
- package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
- package/dist/{cli-jb30dzvk.js → cli-48gxjmcz.js} +530 -190
- package/dist/{cli-pzatvw94.js → cli-54sj64fc.js} +2 -2
- package/dist/{cli-bvpgr9ny.js → cli-60sty27p.js} +5 -5
- package/dist/{cli-asq5ydm4.js → cli-6a9qfa8b.js} +411 -69
- package/dist/{cli-277c31dw.js → cli-6mgdba42.js} +225 -3
- package/dist/{cli-19202kew.js → cli-6srv653c.js} +5 -5
- package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
- package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
- package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
- package/dist/{cli-hh5we352.js → cli-86ehn4s5.js} +7 -7
- package/dist/{cli-9s05422w.js → cli-8mbc9za9.js} +1 -1
- package/dist/{cli-ff1tm9n9.js → cli-8wmj18cz.js} +1 -1
- package/dist/{cli-1r5ad7j5.js → cli-9gsm2rzp.js} +12 -12
- package/dist/{cli-mz9afxxf.js → cli-9pshx6rd.js} +17 -3
- package/dist/{cli-s682zgqz.js → cli-a6ds2epe.js} +1 -1
- package/dist/{cli-6a1xg1p7.js → cli-a89zdqz9.js} +2 -2
- package/dist/{cli-03vpq45j.js → cli-ay40dnj3.js} +2 -2
- package/dist/{cli-tsma7p8v.js → cli-aypd09nf.js} +2 -2
- package/dist/{cli-ethgy7ha.js → cli-b3yzyc57.js} +2 -2
- package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
- package/dist/{cli-mmhj8vd7.js → cli-c668vvcf.js} +11 -2
- package/dist/{cli-sbwsxvtd.js → cli-d20qvpr2.js} +2 -2
- package/dist/{cli-c952c7ac.js → cli-e1595bgx.js} +3 -3
- package/dist/{cli-621ce7kw.js → cli-f12mr20z.js} +3 -3
- package/dist/{cli-dqygeeh5.js → cli-f1e0ptjj.js} +10 -1
- package/dist/{cli-xzfq4at3.js → cli-f4dbh7a5.js} +16 -12
- package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
- package/dist/{cli-5bnsc0e1.js → cli-gsfk58nw.js} +5 -5
- package/dist/cli-gt4h8cxw.js +895 -0
- package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
- package/dist/{cli-25n3mv5d.js → cli-h5jeg46p.js} +6 -6
- package/dist/{cli-gn5af0x5.js → cli-he9gbdx0.js} +62 -12
- package/dist/{cli-gbfxp48k.js → cli-j0ppywdr.js} +42 -35
- package/dist/{cli-k4tjsdx9.js → cli-j3y0x2n4.js} +277 -199
- package/dist/{cli-7p4jxgtt.js → cli-j9bpmykh.js} +75 -27
- package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
- package/dist/{cli-wx2nj19m.js → cli-kv7sbc2c.js} +6 -6
- package/dist/{cli-abs8ndtr.js → cli-m30k9777.js} +26 -26
- package/dist/{cli-cz5nmdxt.js → cli-m5e802r7.js} +2 -2
- package/dist/{cli-0shx9j93.js → cli-pfk92eqz.js} +2 -2
- package/dist/{cli-xtrz6bzs.js → cli-ptfq202x.js} +5 -3
- package/dist/{cli-6n8axjv1.js → cli-qj442fws.js} +2 -2
- package/dist/{cli-c61mbdds.js → cli-rd5m70gj.js} +4 -4
- package/dist/{cli-eftzyh9y.js → cli-s09j9vp5.js} +2 -2
- package/dist/{cli-m4ecdxvz.js → cli-s0mgx214.js} +7 -7
- package/dist/{cli-wghjkr0e.js → cli-sgqm2582.js} +10 -7
- package/dist/{cli-2655k16q.js → cli-sjctjr48.js} +2 -2
- package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
- package/dist/{cli-x0f2v69c.js → cli-twctnkng.js} +12 -7
- package/dist/{cli-j6rktc91.js → cli-v6shg7na.js} +1 -1
- package/dist/{cli-gx4rm0qx.js → cli-vz78s5wm.js} +1 -1
- package/dist/{cli-gwhjmh8g.js → cli-wqr7cea4.js} +5 -5
- package/dist/{cli-e42vss7b.js → cli-x37r7qwf.js} +16 -8
- package/dist/{cli-4b2z6n0m.js → cli-yd2nreqw.js} +17 -8
- package/dist/{cli-64mh9mf3.js → cli-ymjpkxgn.js} +69 -17
- package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
- package/dist/{cli-9j129brq.js → cli-yxbsmx56.js} +6 -6
- package/dist/cli.js +22 -20
- package/dist/{codex-adapter-wch12q99.js → codex-adapter-zk59x2zh.js} +7 -7
- package/dist/{codex-hook-brpnycn5.js → codex-hook-xyjhvxwd.js} +1 -1
- package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
- package/dist/{codex-session-runner-41pfcr95.js → codex-session-runner-kh6s4j3n.js} +8 -8
- package/dist/{commands-9849dkj6.js → commands-68et1qv2.js} +2 -2
- package/dist/{controller-ja0sd95p.js → controller-fyrbnzg4.js} +4 -4
- package/dist/{controllers-connect-xp05dj6x.js → controllers-connect-nep9h2xv.js} +23 -23
- package/dist/{cursor-adapter-623qxqew.js → cursor-adapter-yss3pg7t.js} +2 -2
- package/dist/{db-w759qmx0.js → db-28gjyvxj.js} +4 -2
- package/dist/{db-47vj40dy.js → db-8ftwvzba.js} +3 -3
- package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
- package/dist/{dispatcher-7b8e4z2j.js → dispatcher-hayexmkr.js} +20 -20
- package/dist/{docker-s84hjs91.js → docker-fdzyewpz.js} +2 -2
- package/dist/{dsh-adapter-2ezw3xea.js → dsh-adapter-46wawff9.js} +2 -2
- package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
- package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
- package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-8d1e0h4m.js} +1 -1
- package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
- package/dist/{execute-0xpz8950.js → execute-cjrfxfnm.js} +1 -1
- package/dist/{external-channel-e2s028da.js → external-channel-xbmxcg09.js} +2 -2
- package/dist/{grok-adapter-ys7gaqee.js → grok-adapter-ag4wks2a.js} +10 -10
- package/dist/handlers-tzmevqb5.js +41 -0
- package/dist/{heartbeat-w3mntghj.js → heartbeat-psfvcgwp.js} +23 -23
- package/dist/{hook-g7kvv6x9.js → hook-8pz04697.js} +6 -6
- package/dist/{hook-3jjywnpb.js → hook-cyq4mryn.js} +8 -8
- package/dist/{http-tkp7d4kb.js → http-haj1nd6k.js} +921 -169
- package/dist/{index-q7m26kvs.js → index-jwt2bj6f.js} +265 -97
- package/dist/{index-3n8m9ea2.js → index-q52f47pe.js} +1 -1
- package/dist/{index-31ceyf42.js → index-syvhnvtw.js} +1 -1
- package/dist/index-y1ajx04a.js +43 -0
- package/dist/{index-1ef1myq2.js → index-z5h22pes.js} +20 -20
- package/dist/{index-xdaahthb.js → index-zyq8ct52.js} +20 -20
- package/dist/{keepalive-v4ta8wgg.js → keepalive-xdvsamvj.js} +4 -4
- package/dist/lead-hqg57kdp.js +79 -0
- package/dist/{maintenance-84n1ex21.js → maintenance-4y2xzh3a.js} +1 -1
- package/dist/{oauth-refresh-sweep-vjnnandw.js → oauth-refresh-sweep-jd4r7zbs.js} +9 -4
- package/dist/{onboard-sfmpe4np.js → onboard-18447q9q.js} +6 -6
- package/dist/{opencode-adapter-jmpst4q4.js → opencode-adapter-26b2frdf.js} +7 -7
- package/dist/{otel-impl-v6cc0zve.js → otel-impl-f9tgr8yw.js} +2 -2
- package/dist/{pi-mono-adapter-fk8fkcf6.js → pi-mono-adapter-qn6qbga1.js} +38 -17
- package/dist/{pricing-refresh-fsk1e1zc.js → pricing-refresh-dw2tgb53.js} +3 -3
- package/dist/{rbac-roles-prmkdnkw.js → rbac-roles-aygm2760.js} +2 -2
- package/dist/{rbac-roles-zxmxqa39.js → rbac-roles-s4nwj2m3.js} +1 -1
- package/dist/render-v2-yfvdfr0x.js +16 -0
- package/dist/{resume-frdtgfa0.js → resume-1bmvjbf3.js} +9 -9
- package/dist/{runtime-bt5pkpm1.js → runtime-f7tpzedy.js} +1 -1
- package/dist/{session-access-fpez4y5q.js → session-access-r5garej6.js} +4 -4
- package/dist/session-source-backfill-424wzv0r.js +110 -0
- package/dist/{setup-mq6m3m8q.js → setup-8tygvxkq.js} +1 -1
- package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-xsw323b3.js} +2 -2
- package/dist/{work-objects-kfahhjsq.js → work-objects-ggnghpgf.js} +23 -23
- package/dist/worker-sqjd1nqf.js +79 -0
- package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
- package/openapi.json +1324 -14
- package/package.json +5 -3
- package/src/assets/source-key.ts +95 -0
- package/src/be/automation-preflight.ts +2 -0
- package/src/be/db/api-keys.ts +89 -0
- package/src/be/db/session-labels.ts +28 -0
- package/src/be/db/session-moves.ts +5 -6
- package/src/be/db/session-tree.ts +153 -14
- package/src/be/db/tasks/read.ts +11 -0
- package/src/be/db/tasks/write.ts +6 -1
- package/src/be/db/workflow-runs.ts +219 -6
- package/src/be/db-retention.ts +61 -6
- package/src/be/db.ts +135 -132
- package/src/be/memory/providers/openai-embedding.ts +5 -1
- package/src/be/memory/task-learning.ts +155 -0
- package/src/be/migrations/244_api_key_validation.sql +4 -0
- package/src/be/migrations/245_script_executor.sql +3 -0
- package/src/be/oauth-refresh-sweep.ts +2 -1
- package/src/be/onboarding.ts +4 -0
- package/src/be/scripts/boot-reembed.ts +2 -0
- package/src/be/scripts/db.ts +14 -4
- package/src/be/scripts/embeddings.ts +2 -0
- package/src/be/scripts/run-saved.ts +1 -0
- package/src/be/scripts/typecheck.ts +10 -4
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/session-source-backfill.ts +163 -0
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/comb/review-batch.ts +28 -1
- package/src/comb/templates.ts +7 -2
- package/src/commands/claude-managed-setup.ts +7 -1
- package/src/commands/credential-refresh.ts +76 -1
- package/src/commands/credential-wait.ts +2 -2
- package/src/commands/onboard/compose-generator.ts +5 -3
- package/src/commands/provider-credentials.ts +212 -171
- package/src/commands/runner.ts +25 -4
- package/src/extensions/apply-task-create.ts +8 -1
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +5 -0
- package/src/heartbeat/heartbeat.ts +14 -1
- package/src/heartbeat/script-runner-probe.ts +47 -0
- package/src/http/all-routes.ts +2 -0
- package/src/http/api-keys.ts +77 -9
- package/src/http/assets.ts +15 -0
- package/src/http/codex-oauth-accounts.ts +191 -0
- package/src/http/comb.ts +15 -2
- package/src/http/config.ts +1 -1
- package/src/http/core.ts +6 -0
- package/src/http/fleet-benchmark.ts +5 -2
- package/src/http/idempotency-key.ts +1 -1
- package/src/http/index.ts +14 -3
- package/src/http/integrations.ts +5 -1
- package/src/http/kv.ts +8 -3
- package/src/http/onboarding.ts +6 -1
- package/src/http/pages-public.ts +38 -5
- package/src/http/pool-keys.ts +354 -0
- package/src/http/scripts.ts +27 -0
- package/src/http/sessions.ts +37 -12
- package/src/http/stats.ts +1 -0
- package/src/http/status.ts +26 -12
- package/src/http/tasks.ts +3 -0
- package/src/http/used-by.ts +7 -2
- package/src/http/workflows.ts +88 -9
- package/src/http/x.ts +5 -0
- package/src/integrations/chat-sdk/inbound.ts +27 -0
- package/src/kv-reserved-namespaces.ts +19 -0
- package/src/prompts/base-prompt.ts +21 -1
- package/src/prompts/session-templates.ts +17 -3
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +5 -1
- package/src/providers/claude-managed-adapter.ts +16 -9
- package/src/providers/codex-adapter.ts +71 -11
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +41 -11
- package/src/scheduler/scheduler.ts +5 -3
- package/src/scripts-runtime/eval-harness.ts +8 -1
- package/src/scripts-runtime/executors/native.ts +22 -16
- package/src/scripts-runtime/executors/quickjs-prelude.ts +103 -1
- package/src/scripts-runtime/executors/quickjs-runner.ts +2 -1
- package/src/scripts-runtime/executors/quickjs.ts +7 -0
- package/src/scripts-runtime/executors/registry.ts +32 -0
- package/src/scripts-runtime/executors/types.ts +7 -0
- package/src/scripts-runtime/host-validated-args.ts +133 -0
- package/src/scripts-runtime/loader.ts +30 -3
- package/src/scripts-runtime/swarm-sdk.ts +16 -3
- package/src/scripts-runtime/types/stdlib.d.ts +10 -2
- package/src/scripts-runtime/types/swarm-extension.d.ts +17 -8
- package/src/scripts-runtime/types/swarm-sdk.d.ts +10 -2
- package/src/slack/assistant.ts +7 -0
- package/src/slack/channel-labels.ts +74 -0
- package/src/slack/handlers.ts +8 -0
- package/src/slack/inbound-files.ts +5 -2
- package/src/slack/thread-buffer.ts +4 -0
- package/src/tasks/sibling-awareness.ts +12 -3
- package/src/telemetry-context.ts +5 -6
- package/src/telemetry-identity.ts +3 -3
- package/src/tests/api-key-status-by-suffix.test.ts +242 -0
- package/src/tests/api-keys-validation.test.ts +170 -0
- package/src/tests/base-prompt.test.ts +70 -2
- package/src/tests/claude-managed-adapter.test.ts +46 -0
- package/src/tests/claude-routes-e2e.test.ts +7 -2
- package/src/tests/codex-credential-pools.test.ts +120 -0
- package/src/tests/codex-gateway-route.test.ts +165 -0
- package/src/tests/codex-oauth-accounts.test.ts +357 -0
- package/src/tests/comb-review-batch.test.ts +63 -2
- package/src/tests/community-linear-delivery-loop.test.ts +2099 -0
- package/src/tests/config-pool-keys.test.ts +533 -0
- package/src/tests/credential-check.test.ts +294 -4
- package/src/tests/credential-refresh.test.ts +172 -1
- package/src/tests/db-retention.test.ts +9 -3
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +1 -3
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/docs-index.test.ts +135 -0
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/extensions-pre-task.test.ts +53 -1
- package/src/tests/fetch-resolved-env.test.ts +16 -2
- package/src/tests/fixtures/extensions/record-task-create/hooks.ts +9 -0
- package/src/tests/fixtures/extensions/record-task-create/manifest.json +7 -0
- package/src/tests/fleet-benchmark.test.ts +8 -0
- package/src/tests/get-workflow-run-tool.test.ts +98 -0
- package/src/tests/heartbeat-checklist.test.ts +89 -0
- package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
- package/src/tests/integrations-http.test.ts +32 -2
- package/src/tests/internal-ai/complete-structured.test.ts +37 -1
- package/src/tests/internal-ai/credentials.test.ts +23 -0
- package/src/tests/kv-http.test.ts +32 -0
- package/src/tests/kv-tool.test.ts +25 -0
- package/src/tests/multi-runtime-registration.test.ts +12 -12
- package/src/tests/oauth-refresh-sweep.test.ts +34 -1
- package/src/tests/onboard-compose.test.ts +20 -0
- package/src/tests/onboarding.test.ts +108 -1
- package/src/tests/openai-embedding-credentials.test.ts +35 -0
- package/src/tests/opencode-adapter.test.ts +2 -1
- package/src/tests/openrouter-attribution-call-sites.test.ts +2 -2
- package/src/tests/org-website.test.ts +36 -0
- package/src/tests/pages-frame-ancestors.test.ts +162 -0
- package/src/tests/pi-codemode.test.ts +2 -1
- package/src/tests/pi-mono-adapter.test.ts +54 -0
- package/src/tests/pi-native-mcp.test.ts +13 -1
- package/src/tests/pi-tool-exposure.test.ts +8 -8
- package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
- package/src/tests/prompt-template-session.test.ts +4 -3
- package/src/tests/provider-key-probes.test.ts +181 -0
- package/src/tests/rbac-e2e-helpers.test.ts +88 -1
- package/src/tests/rbac-e2e-helpers.ts +36 -11
- package/src/tests/request-composition-derive.test.ts +329 -0
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/script-executor-conformance.test.ts +36 -1
- package/src/tests/script-executor-quickjs-pool.test.ts +50 -0
- package/src/tests/script-executor-quickjs.test.ts +140 -0
- package/src/tests/script-host-validated-args-native.test.ts +196 -0
- package/src/tests/script-per-script-executor.test.ts +415 -0
- package/src/tests/script-runs-retention.test.ts +212 -0
- package/src/tests/scripts-external-api.test.ts +45 -0
- package/src/tests/sdk-allowlist.test.ts +42 -2
- package/src/tests/self-improvement.test.ts +4 -4
- package/src/tests/session-source-backfill.test.ts +429 -0
- package/src/tests/session-source-keys.test.ts +236 -0
- package/src/tests/session-source-labels.test.ts +419 -0
- package/src/tests/sessions-detail-batch.test.ts +2 -0
- package/src/tests/sessions-move.test.ts +131 -0
- package/src/tests/sessions-tree.test.ts +299 -2
- package/src/tests/slack-inbound-files.test.ts +103 -23
- package/src/tests/status.test.ts +101 -18
- package/src/tests/store-progress-learning.test.ts +288 -0
- package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
- package/src/tests/test-net.ts +31 -0
- package/src/tests/transcription.test.ts +8 -0
- package/src/tests/ui-logs-parser.test.ts +180 -0
- package/src/tests/used-by-route.test.ts +11 -0
- package/src/tests/workflow-cascade-cancel.test.ts +172 -0
- package/src/tests/workflow-engine-v2.test.ts +116 -1
- package/src/tests/workflow-http-v2.test.ts +131 -0
- package/src/tests/workflow-mcp-trigger-schema.test.ts +209 -0
- package/src/tests/workflow-run-finished-event.test.ts +263 -0
- package/src/tests/workflow-step-order.test.ts +92 -0
- package/src/tests/workflow-sub-workflow.test.ts +30 -0
- package/src/tests/workflow-swarm-script.test.ts +34 -0
- package/src/tools/get-tasks.ts +8 -0
- package/src/tools/kv/kv-read-auth.ts +4 -2
- package/src/tools/script-common.ts +1 -0
- package/src/tools/script-run.ts +7 -0
- package/src/tools/script-upsert.ts +7 -0
- package/src/tools/store-progress.ts +67 -1
- package/src/tools/task-action.ts +1 -1
- package/src/tools/workflows/get-workflow-run.ts +14 -4
- package/src/tools/workflows/trigger-workflow.ts +104 -10
- package/src/types.ts +36 -1
- package/src/utils/company-profile.ts +51 -1
- package/src/utils/credentials.ts +10 -4
- package/src/utils/internal-ai/complete-structured.ts +5 -0
- package/src/utils/internal-ai/credentials.ts +4 -3
- package/src/utils/provider-key-probes.ts +328 -0
- package/src/utils/transcription.ts +4 -1
- package/src/workflows/engine.ts +18 -16
- package/src/workflows/executors/sub-workflow.ts +8 -1
- package/src/workflows/executors/swarm-script.ts +5 -0
- package/src/workflows/foreach-driver.ts +5 -11
- package/src/workflows/node-timeout.ts +30 -0
- package/templates/ai-toolbox.manifest.json +2 -2
- package/templates/community/linear-delivery-loop/PLAYBOOK.md +323 -0
- package/templates/community/linear-delivery-loop/README.md +38 -0
- package/templates/community/linear-delivery-loop/config.example.json +31 -0
- package/templates/community/linear-delivery-loop/lead-prompt.md +34 -0
- package/templates/community/linear-delivery-loop/schedule.json +10 -0
- package/templates/community/linear-delivery-loop/scripts/linear-delivery-setup.ts +148 -0
- package/templates/community/linear-delivery-loop/scripts/linear-delivery-tick.ts +1781 -0
- package/templates/skills/researching/content.md +2 -0
- package/templates/skills/swarm-docs/config.json +14 -0
- package/templates/skills/swarm-docs/content.md +144 -0
- package/templates/skills/swarm-scripts/SKILL.md +1 -0
- package/templates/skills/swarm-scripts/content.md +1 -0
- package/dist/cli-7421jzsw.js +0 -84
- package/dist/cli-e45e4m6b.js +0 -413
- package/dist/cli-zw88zjba.js +0 -307
- package/dist/handlers-wy1dr6ja.js +0 -41
- package/dist/index-nmm79qgr.js +0 -43
- package/dist/lead-sawnyqxk.js +0 -79
- package/dist/render-v2-pjwxvnkw.js +0 -12
- package/dist/worker-v5e5rxj3.js +0 -79
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { normalizeSessionLogs as normalizeEvalLogs } from "../../apps/evals/ui/src/logs-parser";
|
|
3
3
|
import {
|
|
4
|
+
cacheHitRatio,
|
|
4
5
|
extractSubagentRuns,
|
|
5
6
|
normalizeSessionLogs,
|
|
6
7
|
parseSessionLogs,
|
|
7
8
|
type SessionLogRecord,
|
|
9
|
+
sumItemCallUsage,
|
|
8
10
|
unwrapResult,
|
|
9
11
|
} from "../../apps/ui/src/logs-parser";
|
|
10
12
|
|
|
@@ -1696,3 +1698,181 @@ describe.each([
|
|
|
1696
1698
|
expect(item?.meta).toMatchObject({ type: "codex_error", isError: true });
|
|
1697
1699
|
});
|
|
1698
1700
|
});
|
|
1701
|
+
|
|
1702
|
+
describe("per-call usage", () => {
|
|
1703
|
+
const claudeUsage = {
|
|
1704
|
+
input_tokens: 6,
|
|
1705
|
+
cache_read_input_tokens: 90_000,
|
|
1706
|
+
cache_creation_input_tokens: 684,
|
|
1707
|
+
output_tokens: 120,
|
|
1708
|
+
};
|
|
1709
|
+
function claudeLine(
|
|
1710
|
+
id: string,
|
|
1711
|
+
line: number,
|
|
1712
|
+
messageId: string,
|
|
1713
|
+
block: unknown,
|
|
1714
|
+
extra: Record<string, unknown> = {},
|
|
1715
|
+
) {
|
|
1716
|
+
return log(id, "claude", line, {
|
|
1717
|
+
type: "assistant",
|
|
1718
|
+
message: { id: messageId, role: "assistant", content: [block], usage: claudeUsage },
|
|
1719
|
+
...extra,
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
const usages = (logs: SessionLogRecord[]) =>
|
|
1723
|
+
normalizeSessionLogs(logs)
|
|
1724
|
+
.items.filter((item) => item.kind === "call_usage")
|
|
1725
|
+
.map((item) => item.usage);
|
|
1726
|
+
|
|
1727
|
+
test("claude: one call_usage per message.id, not per content-block line", () => {
|
|
1728
|
+
const result = usages([
|
|
1729
|
+
claudeLine("a1", 1, "msg_1", { type: "thinking", thinking: "" }),
|
|
1730
|
+
claudeLine("a2", 2, "msg_1", { type: "text", text: "Looking." }),
|
|
1731
|
+
claudeLine("a3", 3, "msg_1", { type: "tool_use", id: "t1", name: "Read", input: {} }),
|
|
1732
|
+
claudeLine(
|
|
1733
|
+
"s1",
|
|
1734
|
+
4,
|
|
1735
|
+
"msg_2",
|
|
1736
|
+
{ type: "text", text: "sub" },
|
|
1737
|
+
{
|
|
1738
|
+
parent_tool_use_id: "toolu_agent",
|
|
1739
|
+
},
|
|
1740
|
+
),
|
|
1741
|
+
]);
|
|
1742
|
+
expect(result).toHaveLength(2);
|
|
1743
|
+
expect(result[0]).toMatchObject({
|
|
1744
|
+
callId: "msg_1",
|
|
1745
|
+
promptTokens: 90_690,
|
|
1746
|
+
cacheReadTokens: 90_000,
|
|
1747
|
+
cacheWriteTokens: 684,
|
|
1748
|
+
uncachedInputTokens: 6,
|
|
1749
|
+
outputTokens: 120,
|
|
1750
|
+
sidechain: false,
|
|
1751
|
+
harness: "claude",
|
|
1752
|
+
reportedAt: "start",
|
|
1753
|
+
});
|
|
1754
|
+
expect(result[1]).toMatchObject({ callId: "msg_2", sidechain: true });
|
|
1755
|
+
expect(cacheHitRatio(result[0]!)).toBeCloseTo(90_000 / 90_690);
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1758
|
+
test("claude: an assistant line without usage emits no call_usage", () => {
|
|
1759
|
+
const result = usages([
|
|
1760
|
+
log("a1", "claude", 1, {
|
|
1761
|
+
type: "assistant",
|
|
1762
|
+
message: { id: "msg_1", role: "assistant", content: [{ type: "text", text: "hi" }] },
|
|
1763
|
+
}),
|
|
1764
|
+
]);
|
|
1765
|
+
expect(result).toEqual([]);
|
|
1766
|
+
});
|
|
1767
|
+
|
|
1768
|
+
test("the call_usage block shares the call's message, ahead of its content", () => {
|
|
1769
|
+
const messages = parseSessionLogs([
|
|
1770
|
+
claudeLine("a1", 1, "msg_1", { type: "text", text: "Looking." }),
|
|
1771
|
+
]);
|
|
1772
|
+
expect(messages).toHaveLength(1);
|
|
1773
|
+
expect(messages[0]?.role).toBe("assistant");
|
|
1774
|
+
expect(messages[0]?.content.map((b) => b.type)).toEqual(["provider_meta", "text"]);
|
|
1775
|
+
expect(messages[0]?.content[0]).toMatchObject({
|
|
1776
|
+
kind: "helper",
|
|
1777
|
+
data: { helperType: "call_usage" },
|
|
1778
|
+
});
|
|
1779
|
+
});
|
|
1780
|
+
|
|
1781
|
+
test("codex: context.updated input_tokens already includes cached tokens", () => {
|
|
1782
|
+
const result = usages([
|
|
1783
|
+
log("c1", "codex", 1, {
|
|
1784
|
+
type: "context.updated",
|
|
1785
|
+
usage: {
|
|
1786
|
+
input_tokens: 49_300,
|
|
1787
|
+
cached_input_tokens: 12_000,
|
|
1788
|
+
cache_write_input_tokens: 0,
|
|
1789
|
+
output_tokens: 300,
|
|
1790
|
+
reasoning_output_tokens: 40,
|
|
1791
|
+
},
|
|
1792
|
+
contextWindow: 272_000,
|
|
1793
|
+
}),
|
|
1794
|
+
]);
|
|
1795
|
+
expect(result).toEqual([
|
|
1796
|
+
{
|
|
1797
|
+
callId: "c1",
|
|
1798
|
+
promptTokens: 49_300,
|
|
1799
|
+
cacheReadTokens: 12_000,
|
|
1800
|
+
cacheWriteTokens: 0,
|
|
1801
|
+
uncachedInputTokens: 37_300,
|
|
1802
|
+
outputTokens: 300,
|
|
1803
|
+
reasoningTokens: 40,
|
|
1804
|
+
sidechain: false,
|
|
1805
|
+
harness: "codex",
|
|
1806
|
+
reportedAt: "end",
|
|
1807
|
+
},
|
|
1808
|
+
]);
|
|
1809
|
+
expect(cacheHitRatio(result[0]!)).toBeCloseTo(12_000 / 49_300);
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
test("opencode: session.step.ended input excludes cache", () => {
|
|
1813
|
+
const result = usages([
|
|
1814
|
+
log("o1", "opencode", 1, {
|
|
1815
|
+
type: "session.step.ended",
|
|
1816
|
+
data: {
|
|
1817
|
+
sessionID: "ses_1",
|
|
1818
|
+
tokens: { input: 200, output: 50, reasoning: 5, cache: { read: 9_000, write: 800 } },
|
|
1819
|
+
},
|
|
1820
|
+
}),
|
|
1821
|
+
]);
|
|
1822
|
+
expect(result).toHaveLength(1);
|
|
1823
|
+
expect(result[0]).toMatchObject({
|
|
1824
|
+
promptTokens: 10_000,
|
|
1825
|
+
cacheReadTokens: 9_000,
|
|
1826
|
+
cacheWriteTokens: 800,
|
|
1827
|
+
uncachedInputTokens: 200,
|
|
1828
|
+
harness: "opencode",
|
|
1829
|
+
reportedAt: "end",
|
|
1830
|
+
});
|
|
1831
|
+
expect(cacheHitRatio(result[0]!)).toBeCloseTo(0.9);
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
test("pi: the adapter's call_usage line", () => {
|
|
1835
|
+
const result = usages([
|
|
1836
|
+
log("p1", "pi", 1, {
|
|
1837
|
+
type: "call_usage",
|
|
1838
|
+
harness: "pi",
|
|
1839
|
+
usage: { input: 100, cacheRead: 300, cacheWrite: 100, output: 20, reasoning: 0 },
|
|
1840
|
+
}),
|
|
1841
|
+
]);
|
|
1842
|
+
expect(result).toHaveLength(1);
|
|
1843
|
+
expect(result[0]).toMatchObject({
|
|
1844
|
+
promptTokens: 500,
|
|
1845
|
+
cacheReadTokens: 300,
|
|
1846
|
+
harness: "pi",
|
|
1847
|
+
reportedAt: "start",
|
|
1848
|
+
});
|
|
1849
|
+
expect(cacheHitRatio(result[0]!)).toBeCloseTo(0.6);
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
test("session totals: Σ read / Σ prompt, and no-cache sessions say so", () => {
|
|
1853
|
+
const totals = sumItemCallUsage(
|
|
1854
|
+
normalizeSessionLogs([
|
|
1855
|
+
claudeLine("a1", 1, "msg_1", { type: "text", text: "one" }),
|
|
1856
|
+
claudeLine("a2", 2, "msg_2", { type: "text", text: "two" }),
|
|
1857
|
+
]).items,
|
|
1858
|
+
);
|
|
1859
|
+
expect(totals).toMatchObject({
|
|
1860
|
+
calls: 2,
|
|
1861
|
+
promptTokens: 181_380,
|
|
1862
|
+
cacheReadTokens: 180_000,
|
|
1863
|
+
cacheReported: true,
|
|
1864
|
+
});
|
|
1865
|
+
expect(totals?.ratio).toBeCloseTo(180_000 / 181_380);
|
|
1866
|
+
|
|
1867
|
+
const uncached = sumItemCallUsage(
|
|
1868
|
+
normalizeSessionLogs([
|
|
1869
|
+
log("o1", "opencode", 1, {
|
|
1870
|
+
type: "session.step.ended",
|
|
1871
|
+
data: { tokens: { input: 500, output: 5, reasoning: 0, cache: { read: 0, write: 0 } } },
|
|
1872
|
+
}),
|
|
1873
|
+
]).items,
|
|
1874
|
+
);
|
|
1875
|
+
expect(uncached).toMatchObject({ calls: 1, cacheReported: false, ratio: 0 });
|
|
1876
|
+
expect(sumItemCallUsage([])).toBeNull();
|
|
1877
|
+
});
|
|
1878
|
+
});
|
|
@@ -183,6 +183,17 @@ describe("POST /api/used-by", () => {
|
|
|
183
183
|
expect(proxied[1]?.body).not.toHaveProperty("quote");
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
+
test("a job function the catalog does not list yet is stored but not sent", async () => {
|
|
187
|
+
const grant = await post(
|
|
188
|
+
{ granted: true, contactEmail: "admin@acme.com", contactFunction: "marketing" },
|
|
189
|
+
OPERATOR,
|
|
190
|
+
);
|
|
191
|
+
expect(grant.statusCode).toBe(200);
|
|
192
|
+
expect(proxied[0]?.body).toMatchObject({ granted: true, contact_email: "admin@acme.com" });
|
|
193
|
+
expect(proxied[0]?.body).not.toHaveProperty("contact_function");
|
|
194
|
+
expect(await configValue("SWARM_USED_BY_CONTACT_FUNCTION")).toBe("marketing");
|
|
195
|
+
});
|
|
196
|
+
|
|
186
197
|
test("a grant requires valid contact details and a quote of at most 280 characters", async () => {
|
|
187
198
|
for (const details of [
|
|
188
199
|
{},
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
WorkflowStepNotExecutableError,
|
|
27
27
|
} from "../be/db/workflow-runs";
|
|
28
28
|
import { handleApprovalRequests } from "../http/approval-requests";
|
|
29
|
+
import { handleWorkflows } from "../http/workflows";
|
|
29
30
|
import * as slackApp from "../slack/app";
|
|
30
31
|
import type { AgentTask, Workflow, WorkflowDefinition, WorkflowRun } from "../types";
|
|
31
32
|
import * as approvalNotifications from "../workflows/approval-notifications";
|
|
@@ -1167,3 +1168,174 @@ describe("run lineage guard", () => {
|
|
|
1167
1168
|
).toBe("failed");
|
|
1168
1169
|
});
|
|
1169
1170
|
});
|
|
1171
|
+
|
|
1172
|
+
describe("run tree read (GET /api/workflow-runs/{id}/tree)", () => {
|
|
1173
|
+
let server: ReturnType<typeof createServer>;
|
|
1174
|
+
let baseUrl = "";
|
|
1175
|
+
|
|
1176
|
+
beforeAll(async () => {
|
|
1177
|
+
server = createServer(async (req, res) => {
|
|
1178
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
1179
|
+
const handled = await handleWorkflows(
|
|
1180
|
+
req,
|
|
1181
|
+
res,
|
|
1182
|
+
url.pathname.split("/").filter(Boolean),
|
|
1183
|
+
url.searchParams,
|
|
1184
|
+
undefined,
|
|
1185
|
+
);
|
|
1186
|
+
if (!handled) {
|
|
1187
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1188
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
baseUrl = `http://127.0.0.1:${await listenOnFreePort(server)}`;
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
afterAll(async () => {
|
|
1195
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
type TreeBody = {
|
|
1199
|
+
rootRunId: string;
|
|
1200
|
+
runs: Array<WorkflowRun & Record<string, unknown>>;
|
|
1201
|
+
hasOpenWork: boolean;
|
|
1202
|
+
error?: string;
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
async function getTree(runId: string): Promise<{ status: number; body: TreeBody | null }> {
|
|
1206
|
+
const response = await fetch(`${baseUrl}/api/workflow-runs/${runId}/tree`);
|
|
1207
|
+
const text = await response.text();
|
|
1208
|
+
return { status: response.status, body: text ? (JSON.parse(text) as TreeBody) : null };
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
test("lists every run of every attempt, root first, and settles after a cascade cancel", async () => {
|
|
1212
|
+
const grandchild = await makeWorkflow(taskDef);
|
|
1213
|
+
const child = await makeWorkflow(parentOf(grandchild.id));
|
|
1214
|
+
const root = await makeWorkflow(parentOf(child.id));
|
|
1215
|
+
const rootId = await startWorkflowExecution(root, {}, registry);
|
|
1216
|
+
const childRun = await runOf(child.id, "waiting");
|
|
1217
|
+
const grandchildRun = await runOf(grandchild.id, "waiting");
|
|
1218
|
+
const [task] = await waitFor(
|
|
1219
|
+
() => tasksOfRun(grandchildRun.id),
|
|
1220
|
+
(tasks) => tasks.length === 1,
|
|
1221
|
+
);
|
|
1222
|
+
|
|
1223
|
+
// A retry of the root step: the live child is attempt 1, and attempt 0's
|
|
1224
|
+
// child failed after starting a grandchild of its own.
|
|
1225
|
+
const [rootStep] = await getWorkflowRunStepsByRunId(rootId);
|
|
1226
|
+
await getDbClient().run("UPDATE workflow_run_steps SET retryCount = 1 WHERE id = ?", [
|
|
1227
|
+
rootStep!.id,
|
|
1228
|
+
]);
|
|
1229
|
+
await getDbClient().run("UPDATE workflow_runs SET parentStepAttempt = 1 WHERE id = ?", [
|
|
1230
|
+
childRun.id,
|
|
1231
|
+
]);
|
|
1232
|
+
const oldChild = await linkedRun({
|
|
1233
|
+
workflowId: child.id,
|
|
1234
|
+
status: "failed",
|
|
1235
|
+
parentStepId: rootStep!.id,
|
|
1236
|
+
parentStepAttempt: 0,
|
|
1237
|
+
});
|
|
1238
|
+
const oldChildStep = await linkedStep(oldChild, {
|
|
1239
|
+
nodeId: "child0",
|
|
1240
|
+
nodeType: "sub-workflow",
|
|
1241
|
+
status: "failed",
|
|
1242
|
+
});
|
|
1243
|
+
const oldGrandchild = await linkedRun({
|
|
1244
|
+
workflowId: grandchild.id,
|
|
1245
|
+
status: "failed",
|
|
1246
|
+
parentStepId: oldChildStep,
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
const before = await getTree(rootId);
|
|
1250
|
+
expect(before.status).toBe(200);
|
|
1251
|
+
expect(before.body!.rootRunId).toBe(rootId);
|
|
1252
|
+
expect(before.body!.hasOpenWork).toBe(true);
|
|
1253
|
+
const ids = before.body!.runs.map((run) => run.id);
|
|
1254
|
+
expect(ids[0]).toBe(rootId);
|
|
1255
|
+
expect(new Set(ids.slice(1, 3))).toEqual(new Set([childRun.id, oldChild]));
|
|
1256
|
+
expect(new Set(ids.slice(3))).toEqual(new Set([grandchildRun.id, oldGrandchild]));
|
|
1257
|
+
const byId = new Map(before.body!.runs.map((run) => [run.id, run]));
|
|
1258
|
+
expect(byId.get(childRun.id)).toMatchObject({
|
|
1259
|
+
workflowId: child.id,
|
|
1260
|
+
status: "waiting",
|
|
1261
|
+
parentStepId: rootStep!.id,
|
|
1262
|
+
parentStepAttempt: 1,
|
|
1263
|
+
});
|
|
1264
|
+
expect(byId.get(oldChild)).toMatchObject({ status: "failed", parentStepAttempt: 0 });
|
|
1265
|
+
expect(byId.get(oldGrandchild)?.parentStepId).toBe(oldChildStep);
|
|
1266
|
+
for (const run of before.body!.runs) expect("context" in run).toBe(false);
|
|
1267
|
+
|
|
1268
|
+
await cancelWorkflowRun(rootId, "stop the tree");
|
|
1269
|
+
expect((await getTaskById(task!.id))?.status).toBe("cancelled");
|
|
1270
|
+
|
|
1271
|
+
const after = await getTree(rootId);
|
|
1272
|
+
expect(after.status).toBe(200);
|
|
1273
|
+
expect(after.body!.hasOpenWork).toBe(false);
|
|
1274
|
+
expect(after.body!.runs.map((run) => run.id)).toEqual(ids);
|
|
1275
|
+
const statuses = new Map(after.body!.runs.map((run) => [run.id, run.status]));
|
|
1276
|
+
expect(statuses.get(rootId)).toBe("cancelled");
|
|
1277
|
+
expect(statuses.get(childRun.id)).toBe("cancelled");
|
|
1278
|
+
expect(statuses.get(grandchildRun.id)).toBe("cancelled");
|
|
1279
|
+
// Terminal history keeps its status.
|
|
1280
|
+
expect(statuses.get(oldChild)).toBe("failed");
|
|
1281
|
+
expect(statuses.get(oldGrandchild)).toBe("failed");
|
|
1282
|
+
|
|
1283
|
+
// A subtree reads on its own, without its ancestors.
|
|
1284
|
+
const subtree = await getTree(childRun.id);
|
|
1285
|
+
expect(subtree.body!.runs.map((run) => run.id)).toEqual([childRun.id, grandchildRun.id]);
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
test("a tree wider than one summary query returns every run once, in walk order", async () => {
|
|
1289
|
+
const wf = await makeWorkflow({ nodes: [] });
|
|
1290
|
+
const rootId = await linkedRun({ workflowId: wf.id, status: "completed" });
|
|
1291
|
+
const step = await linkedStep(rootId, {
|
|
1292
|
+
nodeId: "fan",
|
|
1293
|
+
nodeType: "sub-workflow",
|
|
1294
|
+
status: "completed",
|
|
1295
|
+
});
|
|
1296
|
+
// One child per attempt, over two 500-id summary chunks, so a dropped or
|
|
1297
|
+
// repeated chunk shows.
|
|
1298
|
+
const childCount = 1_100;
|
|
1299
|
+
await getDbClient().transaction(async () => {
|
|
1300
|
+
for (let i = 0; i < childCount; i++) {
|
|
1301
|
+
await createWorkflowRun({
|
|
1302
|
+
id: crypto.randomUUID(),
|
|
1303
|
+
workflowId: wf.id,
|
|
1304
|
+
parentStepId: step,
|
|
1305
|
+
parentStepAttempt: i,
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
const walk = await getWorkflowRunTreeIds(rootId);
|
|
1311
|
+
expect(walk).toHaveLength(childCount + 1);
|
|
1312
|
+
const tree = await getTree(rootId);
|
|
1313
|
+
expect(tree.status).toBe(200);
|
|
1314
|
+
expect(tree.body!.runs.map((run) => run.id)).toEqual(walk);
|
|
1315
|
+
expect(tree.body!.hasOpenWork).toBe(true);
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
test("a run without children is its own tree", async () => {
|
|
1319
|
+
const wf = await makeWorkflow({ nodes: [] });
|
|
1320
|
+
const runId = await linkedRun({ workflowId: wf.id, status: "completed" });
|
|
1321
|
+
const tree = await getTree(runId);
|
|
1322
|
+
expect(tree.status).toBe(200);
|
|
1323
|
+
expect(tree.body!.runs.map((run) => run.id)).toEqual([runId]);
|
|
1324
|
+
expect(tree.body!.hasOpenWork).toBe(false);
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
test("an unknown run is 404 and a cycle in the stored links is 409", async () => {
|
|
1328
|
+
expect((await getTree(crypto.randomUUID())).status).toBe(404);
|
|
1329
|
+
|
|
1330
|
+
const wf = await makeWorkflow({ nodes: [] });
|
|
1331
|
+
const a = await linkedRun({ workflowId: wf.id, status: "waiting" });
|
|
1332
|
+
const aStep = await linkedStep(a, { nodeId: "s", nodeType: "sub-workflow", status: "waiting" });
|
|
1333
|
+
const b = await linkedRun({ workflowId: wf.id, status: "waiting", parentStepId: aStep });
|
|
1334
|
+
const bStep = await linkedStep(b, { nodeId: "s", nodeType: "sub-workflow", status: "waiting" });
|
|
1335
|
+
await getDbClient().run("UPDATE workflow_runs SET parentStepId = ? WHERE id = ?", [bStep, a]);
|
|
1336
|
+
|
|
1337
|
+
const cycle = await getTree(a);
|
|
1338
|
+
expect(cycle.status).toBe(409);
|
|
1339
|
+
expect(cycle.body!.error).toContain("cycle");
|
|
1340
|
+
});
|
|
1341
|
+
});
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
getWorkflowRunStepsByRunId,
|
|
22
22
|
initDb,
|
|
23
23
|
} from "../be/db";
|
|
24
|
-
import type { Workflow, WorkflowDefinition } from "../types";
|
|
24
|
+
import type { ExecutorMeta, Workflow, WorkflowDefinition } from "../types";
|
|
25
25
|
import { shouldSkipCooldown } from "../workflows/cooldown";
|
|
26
26
|
import {
|
|
27
27
|
findReadyNodes,
|
|
@@ -143,6 +143,45 @@ class SlowExecutor extends BaseExecutor<typeof SlowExecutor.schema, typeof SlowE
|
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/** What the last `signal-probe` step saw: its abort signal and whether it got to its side effect. */
|
|
147
|
+
const signalProbe: { signal?: AbortSignal; sideEffect: boolean } = { sideEffect: false };
|
|
148
|
+
|
|
149
|
+
class SignalProbeExecutor extends BaseExecutor<
|
|
150
|
+
typeof SignalProbeExecutor.schema,
|
|
151
|
+
typeof SignalProbeExecutor.outSchema
|
|
152
|
+
> {
|
|
153
|
+
static readonly schema = z.object({ delayMs: z.number() });
|
|
154
|
+
static readonly outSchema = z.object({ finished: z.boolean() });
|
|
155
|
+
|
|
156
|
+
readonly type = "signal-probe";
|
|
157
|
+
readonly mode = "instant" as const;
|
|
158
|
+
readonly configSchema = SignalProbeExecutor.schema;
|
|
159
|
+
readonly outputSchema = SignalProbeExecutor.outSchema;
|
|
160
|
+
|
|
161
|
+
protected async execute(
|
|
162
|
+
config: z.infer<typeof SignalProbeExecutor.schema>,
|
|
163
|
+
_context: Readonly<Record<string, unknown>>,
|
|
164
|
+
meta: ExecutorMeta,
|
|
165
|
+
): Promise<ExecutorResult<z.infer<typeof SignalProbeExecutor.outSchema>>> {
|
|
166
|
+
const signal = meta.signal;
|
|
167
|
+
signalProbe.signal = signal;
|
|
168
|
+
await new Promise<void>((resolve) => {
|
|
169
|
+
const timer = setTimeout(resolve, config.delayMs);
|
|
170
|
+
signal?.addEventListener(
|
|
171
|
+
"abort",
|
|
172
|
+
() => {
|
|
173
|
+
clearTimeout(timer);
|
|
174
|
+
resolve();
|
|
175
|
+
},
|
|
176
|
+
{ once: true },
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
// A one-shot action, like the review trigger a claim script fires.
|
|
180
|
+
if (!signal?.aborted) signalProbe.sideEffect = true;
|
|
181
|
+
return { status: "success", output: { finished: true } };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
146
185
|
class FailingExecutor extends BaseExecutor<
|
|
147
186
|
typeof FailingExecutor.schema,
|
|
148
187
|
typeof FailingExecutor.outSchema
|
|
@@ -216,6 +255,7 @@ function createTestRegistry(): ExecutorRegistry {
|
|
|
216
255
|
registry.register(new SlowExecutor(mockDeps));
|
|
217
256
|
registry.register(new FailingExecutor(mockDeps));
|
|
218
257
|
registry.register(new ValidatePassExecutor(mockDeps));
|
|
258
|
+
registry.register(new SignalProbeExecutor(mockDeps));
|
|
219
259
|
return registry;
|
|
220
260
|
}
|
|
221
261
|
|
|
@@ -561,6 +601,41 @@ describe("Workflow Engine v2 (Phase 3)", () => {
|
|
|
561
601
|
expect(run!.error).toContain("timed out");
|
|
562
602
|
}, 5_000); // Allow test up to 5s
|
|
563
603
|
|
|
604
|
+
test("a timed-out step aborts its executor, so the side effect never happens", async () => {
|
|
605
|
+
signalProbe.signal = undefined;
|
|
606
|
+
signalProbe.sideEffect = false;
|
|
607
|
+
const registry = createTestRegistry();
|
|
608
|
+
const workflow = await makeWorkflow({
|
|
609
|
+
nodes: [{ id: "probe", type: "signal-probe", config: { delayMs: 600, timeoutMs: 100 } }],
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const runId = await startWorkflowExecution(workflow, {}, registry);
|
|
613
|
+
|
|
614
|
+
const run = await getWorkflowRun(runId);
|
|
615
|
+
expect(run!.status).toBe("failed");
|
|
616
|
+
expect(run!.error).toContain("Step timed out after 100ms");
|
|
617
|
+
expect(signalProbe.signal?.aborted).toBe(true);
|
|
618
|
+
// Past the executor's own delay: without the abort it would have fired.
|
|
619
|
+
await Bun.sleep(700);
|
|
620
|
+
expect(signalProbe.sideEffect).toBe(false);
|
|
621
|
+
}, 5_000);
|
|
622
|
+
|
|
623
|
+
test("a step that finishes in budget is never aborted afterwards", async () => {
|
|
624
|
+
signalProbe.signal = undefined;
|
|
625
|
+
signalProbe.sideEffect = false;
|
|
626
|
+
const registry = createTestRegistry();
|
|
627
|
+
const workflow = await makeWorkflow({
|
|
628
|
+
nodes: [{ id: "probe", type: "signal-probe", config: { delayMs: 10, timeoutMs: 150 } }],
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
const runId = await startWorkflowExecution(workflow, {}, registry);
|
|
632
|
+
|
|
633
|
+
expect((await getWorkflowRun(runId))!.status).toBe("completed");
|
|
634
|
+
expect(signalProbe.sideEffect).toBe(true);
|
|
635
|
+
await Bun.sleep(250);
|
|
636
|
+
expect(signalProbe.signal?.aborted).toBe(false);
|
|
637
|
+
}, 5_000);
|
|
638
|
+
|
|
564
639
|
// The inline runtime's ulimit preamble fails on macOS (no usable
|
|
565
640
|
// RLIMIT_AS), so the script dies before the watchdog behavior under test.
|
|
566
641
|
// Linux CI is the enforcing environment; the skip only unblocks local
|
|
@@ -598,6 +673,46 @@ describe("Workflow Engine v2 (Phase 3)", () => {
|
|
|
598
673
|
|
|
599
674
|
// ─── Validation ───────────────────────────────────────────
|
|
600
675
|
|
|
676
|
+
describe("Detached start", () => {
|
|
677
|
+
test("detach returns once the run is committed and walks its instant steps in the background", async () => {
|
|
678
|
+
const registry = createTestRegistry();
|
|
679
|
+
const workflow = await makeWorkflow({
|
|
680
|
+
nodes: [
|
|
681
|
+
{ id: "slow", type: "slow", config: { delayMs: 400 }, next: "after" },
|
|
682
|
+
{ id: "after", type: "echo", config: { message: "done" } },
|
|
683
|
+
],
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
const started = performance.now();
|
|
687
|
+
const runId = await startWorkflowExecution(workflow, {}, registry, { detach: true });
|
|
688
|
+
const returnedAfterMs = performance.now() - started;
|
|
689
|
+
|
|
690
|
+
expect(returnedAfterMs).toBeLessThan(300);
|
|
691
|
+
expect((await getWorkflowRun(runId))!.status).toBe("running");
|
|
692
|
+
|
|
693
|
+
const deadline = Date.now() + 3_000;
|
|
694
|
+
while ((await getWorkflowRun(runId))!.status === "running" && Date.now() < deadline) {
|
|
695
|
+
await Bun.sleep(25);
|
|
696
|
+
}
|
|
697
|
+
expect((await getWorkflowRun(runId))!.status).toBe("completed");
|
|
698
|
+
const steps = await getWorkflowRunStepsByRunId(runId);
|
|
699
|
+
expect(steps.map((step) => step.nodeId).sort()).toEqual(["after", "slow"]);
|
|
700
|
+
}, 5_000);
|
|
701
|
+
|
|
702
|
+
test("without detach the start resolves only after the instant steps ran", async () => {
|
|
703
|
+
const registry = createTestRegistry();
|
|
704
|
+
const workflow = await makeWorkflow({
|
|
705
|
+
nodes: [{ id: "slow", type: "slow", config: { delayMs: 300 } }],
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
const started = performance.now();
|
|
709
|
+
const runId = await startWorkflowExecution(workflow, {}, registry);
|
|
710
|
+
|
|
711
|
+
expect(performance.now() - started).toBeGreaterThanOrEqual(290);
|
|
712
|
+
expect((await getWorkflowRun(runId))!.status).toBe("completed");
|
|
713
|
+
}, 5_000);
|
|
714
|
+
});
|
|
715
|
+
|
|
601
716
|
describe("Validation", () => {
|
|
602
717
|
test("validation pass allows step to complete", async () => {
|
|
603
718
|
const registry = createTestRegistry();
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
initDb,
|
|
21
21
|
listWorkflowRuns,
|
|
22
22
|
updateWorkflowRun,
|
|
23
|
+
updateWorkflowRunStep,
|
|
23
24
|
} from "../be/db";
|
|
24
25
|
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
25
26
|
import { handleWorkflows } from "../http/workflows";
|
|
@@ -1007,6 +1008,136 @@ describe("Workflow HTTP API v2", () => {
|
|
|
1007
1008
|
});
|
|
1008
1009
|
expect(res.status).toBe(404);
|
|
1009
1010
|
});
|
|
1011
|
+
|
|
1012
|
+
type RunDetail = {
|
|
1013
|
+
run: WorkflowRun;
|
|
1014
|
+
steps: WorkflowRunStep[];
|
|
1015
|
+
parent?: { runId: string; workflowId: string; stepId: string; nodeId: string };
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
async function getRunDetail(runId: string, fields?: "full" | "slim"): Promise<RunDetail> {
|
|
1019
|
+
const query = fields ? `?fields=${fields}` : "";
|
|
1020
|
+
const res = await fetch(`${baseUrl}/api/workflow-runs/${runId}${query}`, { headers });
|
|
1021
|
+
expect(res.status).toBe(200);
|
|
1022
|
+
return (await res.json()) as RunDetail;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/** A finished run whose `poll` node ran `iterations` times, each step snapshotting the context. */
|
|
1026
|
+
async function createLoopRun(iterations: number) {
|
|
1027
|
+
const workflow = await createTestWorkflow();
|
|
1028
|
+
const runId = crypto.randomUUID();
|
|
1029
|
+
await createWorkflowRun({ id: runId, workflowId: workflow.id });
|
|
1030
|
+
const context = {
|
|
1031
|
+
trigger: { repo: "a/b" },
|
|
1032
|
+
poll: { attempts: iterations },
|
|
1033
|
+
blob: "x".repeat(4_000),
|
|
1034
|
+
};
|
|
1035
|
+
for (let i = 0; i < iterations; i++) {
|
|
1036
|
+
const step = await createWorkflowRunStep({
|
|
1037
|
+
id: crypto.randomUUID(),
|
|
1038
|
+
runId,
|
|
1039
|
+
nodeId: "poll",
|
|
1040
|
+
nodeType: "script",
|
|
1041
|
+
input: { ...context, iteration: i },
|
|
1042
|
+
idempotencyKey: `${runId}:poll:${i}`,
|
|
1043
|
+
});
|
|
1044
|
+
await updateWorkflowRunStep(step.id, {
|
|
1045
|
+
status: i === iterations - 1 ? "completed" : "failed",
|
|
1046
|
+
output: { iteration: i, ready: i === iterations - 1 },
|
|
1047
|
+
error: i === iterations - 1 ? null : `not ready (${i})`,
|
|
1048
|
+
diagnostics: `iteration ${i}`,
|
|
1049
|
+
retryCount: i,
|
|
1050
|
+
nextRetryAt: new Date(Date.now() + 60_000).toISOString(),
|
|
1051
|
+
finishedAt: new Date().toISOString(),
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
await updateWorkflowRun(runId, {
|
|
1055
|
+
status: "completed",
|
|
1056
|
+
context,
|
|
1057
|
+
finishedAt: new Date().toISOString(),
|
|
1058
|
+
});
|
|
1059
|
+
return { workflow, runId, context };
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
test("fields=slim omits run.context and steps[].input, keeps every other field", async () => {
|
|
1063
|
+
const { runId, context } = await createLoopRun(3);
|
|
1064
|
+
|
|
1065
|
+
const byDefault = await getRunDetail(runId);
|
|
1066
|
+
const full = await getRunDetail(runId, "full");
|
|
1067
|
+
const slim = await getRunDetail(runId, "slim");
|
|
1068
|
+
|
|
1069
|
+
expect(byDefault).toEqual(full);
|
|
1070
|
+
expect(full.run.context).toEqual(context);
|
|
1071
|
+
expect(full.steps).toHaveLength(3);
|
|
1072
|
+
for (const step of full.steps) expect(step.input).toBeDefined();
|
|
1073
|
+
|
|
1074
|
+
expect(slim.run).not.toHaveProperty("context");
|
|
1075
|
+
expect(slim.steps).toHaveLength(3);
|
|
1076
|
+
for (const step of slim.steps) expect(step).not.toHaveProperty("input");
|
|
1077
|
+
|
|
1078
|
+
const { context: _context, ...fullRunRest } = full.run;
|
|
1079
|
+
expect(slim.run).toEqual(fullRunRest);
|
|
1080
|
+
const fullStepsRest = full.steps.map(({ input: _input, ...rest }) => rest);
|
|
1081
|
+
// Same steps in the same order: both reads break `startedAt` ties by rowid.
|
|
1082
|
+
expect(slim.steps).toEqual(fullStepsRest);
|
|
1083
|
+
// The retry and result fields the slim view exists to serve.
|
|
1084
|
+
const last = slim.steps.at(-1)!;
|
|
1085
|
+
expect(last.output).toEqual({ iteration: 2, ready: true });
|
|
1086
|
+
expect(last.retryCount).toBe(2);
|
|
1087
|
+
expect(last.diagnostics).toBe("iteration 2");
|
|
1088
|
+
expect(last.nextRetryAt).toBeDefined();
|
|
1089
|
+
expect(slim.steps[0]!.error).toBe("not ready (0)");
|
|
1090
|
+
|
|
1091
|
+
expect(JSON.stringify(slim).length).toBeLessThan(JSON.stringify(full).length / 4);
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
test("fields=slim returns 404 for missing run", async () => {
|
|
1095
|
+
const res = await fetch(`${baseUrl}/api/workflow-runs/${crypto.randomUUID()}?fields=slim`, {
|
|
1096
|
+
headers,
|
|
1097
|
+
});
|
|
1098
|
+
expect(res.status).toBe(404);
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
test("rejects an unknown fields value", async () => {
|
|
1102
|
+
const { runId } = await createLoopRun(1);
|
|
1103
|
+
const res = await fetch(`${baseUrl}/api/workflow-runs/${runId}?fields=tiny`, { headers });
|
|
1104
|
+
expect(res.status).toBe(400);
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
test("child run carries parent in both views", async () => {
|
|
1108
|
+
const { workflow: parentWorkflow, runId: parentRunId } = await createLoopRun(1);
|
|
1109
|
+
const parentStep = await createWorkflowRunStep({
|
|
1110
|
+
id: crypto.randomUUID(),
|
|
1111
|
+
runId: parentRunId,
|
|
1112
|
+
nodeId: "child",
|
|
1113
|
+
nodeType: "sub-workflow",
|
|
1114
|
+
input: { large: "y".repeat(1_000) },
|
|
1115
|
+
});
|
|
1116
|
+
const childWorkflow = await createTestWorkflow();
|
|
1117
|
+
const childRunId = crypto.randomUUID();
|
|
1118
|
+
await createWorkflowRun({
|
|
1119
|
+
id: childRunId,
|
|
1120
|
+
workflowId: childWorkflow.id,
|
|
1121
|
+
parentStepId: parentStep.id,
|
|
1122
|
+
parentStepAttempt: 0,
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
const expectedParent = {
|
|
1126
|
+
runId: parentRunId,
|
|
1127
|
+
workflowId: parentWorkflow.id,
|
|
1128
|
+
stepId: parentStep.id,
|
|
1129
|
+
nodeId: "child",
|
|
1130
|
+
};
|
|
1131
|
+
const full = await getRunDetail(childRunId, "full");
|
|
1132
|
+
const slim = await getRunDetail(childRunId, "slim");
|
|
1133
|
+
expect(full.parent).toEqual(expectedParent);
|
|
1134
|
+
expect(slim.parent).toEqual(expectedParent);
|
|
1135
|
+
expect(slim.run.parentStepId).toBe(parentStep.id);
|
|
1136
|
+
expect(slim.run.parentStepAttempt).toBe(0);
|
|
1137
|
+
|
|
1138
|
+
// A top-level run has no parent in either view.
|
|
1139
|
+
expect(await getRunDetail(parentRunId, "slim")).not.toHaveProperty("parent");
|
|
1140
|
+
});
|
|
1010
1141
|
});
|
|
1011
1142
|
|
|
1012
1143
|
// ─── WEBHOOK ──────────────────────────────────────────────
|