@desplega.ai/agent-swarm 2.7.4 → 2.7.6
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 +1 -1
- package/dist/acp-adapter-bp91d37f.js +15 -0
- package/dist/{actions-gfrr9vb7.js → actions-yj0eeaqh.js} +19 -18
- package/dist/{amp-adapter-0s58gm46.js → amp-adapter-4pg9h34q.js} +3 -3
- package/dist/{app-stvq692g.js → app-909e97cr.js} +3 -3
- package/dist/{assistant-5s37fr9n.js → assistant-qdyb5fyy.js} +32 -23
- package/dist/{boot-reembed-yzy1841n.js → boot-reembed-fk0s9z18.js} +1 -1
- package/dist/{boot-reembed-cv82gz7y.js → boot-reembed-gs1rzrnh.js} +1 -1
- package/dist/{boot-scrub-sweep-8k4q95ap.js → boot-scrub-sweep-4qfd4kfx.js} +4 -4
- package/dist/{boot-seeds-ftt9htah.js → boot-seeds-yr00k13d.js} +4 -4
- package/dist/{boxd-515v1t8z.js → boxd-vdmfssd9.js} +5 -5
- package/dist/{claude-adapter-t00wrzeh.js → claude-adapter-qta1f1xn.js} +7 -7
- package/dist/{claude-managed-adapter-g6vmjccb.js → claude-managed-adapter-dvs2xw5n.js} +2 -2
- package/dist/{claude-managed-setup-afpa82f7.js → claude-managed-setup-sc6j4dm0.js} +3 -3
- package/dist/{claude-sdk-session-vj0c2azx.js → claude-sdk-session-zgrz6yvc.js} +9175 -9076
- package/dist/{cli-gjr2y2xk.js → cli-0agcv1v5.js} +4 -1
- package/dist/{cli-edmm9d9y.js → cli-0at8w34w.js} +7 -7
- package/dist/{cli-a81qevyg.js → cli-0qatpy8q.js} +20 -36
- package/dist/{cli-bcz074zx.js → cli-0yv5z2pg.js} +1 -1
- package/dist/{cli-a4btqqft.js → cli-1b7btd38.js} +1 -1
- package/dist/{cli-a2jw6ee5.js → cli-1p89xny0.js} +1 -1
- package/dist/{cli-v726afdm.js → cli-2pnswgh8.js} +1 -1
- package/dist/{cli-jkt52mgt.js → cli-386tyxxg.js} +2 -2
- package/dist/{cli-az0bpf8p.js → cli-3ypk9vk1.js} +3 -3
- package/dist/{cli-wk9md5px.js → cli-436532ck.js} +4 -4
- package/dist/{cli-7n5sbw0z.js → cli-4necwtsv.js} +5 -5
- package/dist/cli-54t00gt8.js +151 -0
- package/dist/{cli-655xm9ms.js → cli-583frbyr.js} +1 -1
- package/dist/{cli-4r12t6w6.js → cli-5c4hjzne.js} +101 -4
- package/dist/{cli-hf6zt5m6.js → cli-69szn7w7.js} +17 -1
- package/dist/{cli-rtk9g7av.js → cli-73jzwksr.js} +6 -2
- package/dist/{cli-jg0nfda6.js → cli-75gkpjvw.js} +52 -21
- package/dist/{cli-wcmnan2g.js → cli-89knwags.js} +5 -5
- package/dist/{cli-pwyevs29.js → cli-8bc1w0tw.js} +22 -21
- package/dist/{cli-t282jgab.js → cli-8snvgs5r.js} +41 -16
- package/dist/{cli-vqgnyh76.js → cli-98rw5dt5.js} +15 -3
- package/dist/{cli-9cctx1tn.js → cli-a22h291q.js} +1 -1
- package/dist/{cli-ycm6180s.js → cli-a7v854rf.js} +5 -3
- package/dist/{cli-98pke4vk.js → cli-ae7mrdf9.js} +2 -2
- package/dist/{cli-je8yeyc5.js → cli-bjb3z51z.js} +5 -5
- package/dist/{cli-mc97xy2s.js → cli-c60vetmm.js} +16 -4
- package/dist/{cli-j4kw3rsx.js → cli-cm5yqn35.js} +18 -0
- package/dist/{cli-wndv9p7g.js → cli-decfb6v8.js} +6 -6
- package/dist/{cli-h55y2bjc.js → cli-dhar9qay.js} +2 -2
- package/dist/{cli-8yy525ad.js → cli-drr5v6av.js} +2 -2
- package/dist/{cli-5gr1g711.js → cli-dxae2yg7.js} +2 -2
- package/dist/{cli-pe09p6dv.js → cli-e3r2neck.js} +8 -7
- package/dist/{cli-13jvmew3.js → cli-ekwks2vs.js} +7 -7
- package/dist/{cli-jg4p5t36.js → cli-es6va8jk.js} +121 -51
- package/dist/{cli-j4kt1mpb.js → cli-faxe6thg.js} +3 -3
- package/dist/{cli-tzrpv5sc.js → cli-ftyxyspd.js} +1 -1
- package/dist/cli-fw2w0kva.js +47 -0
- package/dist/{cli-2dfbv8qr.js → cli-fy6x31ps.js} +1 -0
- package/dist/{cli-aq58fk5m.js → cli-g83s9ewh.js} +7 -1
- package/dist/cli-gdhpcbhf.js +55 -0
- package/dist/{cli-82jg7fp9.js → cli-he5nxaa8.js} +1 -1
- package/dist/{cli-8veytvbh.js → cli-he9wchme.js} +1 -1
- package/dist/{cli-k76tgq03.js → cli-hgaazk69.js} +2 -2
- package/dist/{cli-d2pzqej1.js → cli-hr2e2fft.js} +741 -7
- package/dist/{cli-dae4sbgy.js → cli-jm2vfpfd.js} +2 -2
- package/dist/{cli-1s1dqn1j.js → cli-k2gzwsj2.js} +297 -140
- package/dist/{cli-b1afgtj3.js → cli-kqdg1bsb.js} +3 -3
- package/dist/cli-m2z8jagn.js +12078 -0
- package/dist/{cli-mvckssyq.js → cli-m48g6p95.js} +5 -5
- package/dist/{cli-bwty35gx.js → cli-mthwvjre.js} +1 -1
- package/dist/{cli-41z8xwj0.js → cli-nhrvpaj7.js} +34 -8
- package/dist/{cli-ahd90bwt.js → cli-nwj0ee2w.js} +6 -6
- package/dist/{cli-kvtc7aez.js → cli-p5gp8pcx.js} +44 -14
- package/dist/{cli-avgcz4ra.js → cli-pd7vagab.js} +436 -171
- package/dist/{cli-zn15c46z.js → cli-q0k1w5zd.js} +1 -1
- package/dist/{cli-bq2pcphn.js → cli-rmmmrjkd.js} +1 -1
- package/dist/{cli-2qpvmmkw.js → cli-s0qnd4wv.js} +1 -1
- package/dist/{cli-kg8hchyg.js → cli-sa02jsqt.js} +1 -1
- package/dist/{cli-1yekbyqm.js → cli-shpbx5ee.js} +283 -82
- package/dist/{cli-9y354g3j.js → cli-srye3vdt.js} +1 -1
- package/dist/{cli-byqfbnjw.js → cli-svj5090z.js} +9 -9
- package/dist/{cli-gcbdjt47.js → cli-vfym7wmz.js} +6 -6
- package/dist/{cli-v1yw3wha.js → cli-vvpx3gsx.js} +4 -4
- package/dist/{cli-a1fhj4n9.js → cli-w6dt9ged.js} +2 -2
- package/dist/{cli-1gm8jcyr.js → cli-waqbxg08.js} +5232 -68
- package/dist/{cli-5hjced2v.js → cli-wp9sp1qb.js} +6 -6
- package/dist/{cli-eny54typ.js → cli-wx3rhnm0.js} +1 -1
- package/dist/{cli-s0exq74r.js → cli-xbbndvjy.js} +3 -3
- package/dist/{cli-f255yaqn.js → cli-xds5f77d.js} +3 -3
- package/dist/{cli-0sbkss7h.js → cli-xnqgpshy.js} +2 -2
- package/dist/{cli-0fgr1w9b.js → cli-xnrwazav.js} +20 -3
- package/dist/{cli-gf5hbmdr.js → cli-yjdgea32.js} +3 -3
- package/dist/{cli-cq1pf336.js → cli-yn4kxs06.js} +124 -11
- package/dist/{cli-224epwwf.js → cli-yx4kfngt.js} +3 -3
- package/dist/{cli-eqx6k993.js → cli-yy3rhsdw.js} +47 -13
- package/dist/{cli-36g23z21.js → cli-zbbwez0r.js} +4 -4
- package/dist/{cli-ktmvb20s.js → cli-zm72anhg.js} +12 -12
- package/dist/cli.js +26 -25
- package/dist/{codex-adapter-g0gctw29.js → codex-adapter-ayd1fb6h.js} +5 -5
- package/dist/{codex-hook-vrzz2xhr.js → codex-hook-9efm2x1m.js} +1 -1
- package/dist/{codex-login-0gg46ygn.js → codex-login-y0pmff1c.js} +1 -1
- package/dist/{codex-session-runner-dr2cxe1c.js → codex-session-runner-jesa32xc.js} +6 -6
- package/dist/{commands-1p07cvff.js → commands-hanewgf2.js} +9 -3
- package/dist/{controller-z0s7sv6r.js → controller-dt1n3c1q.js} +9 -4
- package/dist/{controllers-connect-x9kvvhyk.js → controllers-connect-xx8x64n5.js} +22 -21
- package/dist/{cursor-adapter-m5b98pvk.js → cursor-adapter-9s4dwwdw.js} +2 -2
- package/dist/{db-capk2raj.js → db-3vttcbv4.js} +3 -3
- package/dist/{db-rsh04kh3.js → db-9dv0g1ab.js} +12 -4
- package/dist/{devin-adapter-yxm87fe9.js → devin-adapter-r9phbnc9.js} +1 -1
- package/dist/{devtools-82fzqra8.js → devtools-qxfgbsqz.js} +1 -1
- package/dist/{dispatcher-304q6fwa.js → dispatcher-tx9hd1sv.js} +18 -17
- package/dist/{docker-7rxt0rry.js → docker-c1a3gqmf.js} +91 -24
- package/dist/{dsh-adapter-btewem0y.js → dsh-adapter-007y4ejs.js} +2 -2
- package/dist/{e2b-renc0k8q.js → e2b-a19d28rz.js} +4 -4
- package/dist/{e2b-7fc9b9bd.js → e2b-we6hs6q5.js} +6 -6
- package/dist/{easter-egg-3d-fm1p3n4d.js → easter-egg-3d-rgbrqb8s.js} +1 -1
- package/dist/{embedding-space-s68v49ct.js → embedding-space-ny1e2940.js} +6 -6
- package/dist/{embeddings-4ms3604w.js → embeddings-2t386t13.js} +1 -1
- package/dist/{event-bus-dfjx2qdh.js → event-bus-g0ka3pbw.js} +1 -1
- package/dist/{execute-mcdgccjs.js → execute-pksh0ceh.js} +1 -1
- package/dist/{external-channel-9dxwfztn.js → external-channel-phgmhmd7.js} +2 -2
- package/dist/fx-adapter-fy8rr921.js +18 -0
- package/dist/{google-generative-ai-yvmdxyxa.js → google-generative-ai-8y3m3k4b.js} +1 -1
- package/dist/{google-vertex-jrtdh9gs.js → google-vertex-w3v98n7b.js} +1 -1
- package/dist/{grok-adapter-05ecv4h0.js → grok-adapter-q0abvpaj.js} +8 -8
- package/dist/handlers-by3sfxyx.js +32 -0
- package/dist/{heartbeat-3n94heh3.js → heartbeat-kadyek8p.js} +21 -20
- package/dist/{hook-aadrn69x.js → hook-5zs3msf6.js} +6 -6
- package/dist/{hook-zxwje86c.js → hook-9k551xnj.js} +4 -4
- package/dist/{http-xzxk9mst.js → http-mhz9vw1f.js} +272 -103
- package/dist/index-4f2pvxae.js +37 -0
- package/dist/index-98gz7tmp.js +30 -0
- package/dist/{index-5rk6rfzr.js → index-fvzke5kx.js} +1 -1
- package/dist/index-k08z2cak.js +33 -0
- package/dist/{index-yqhz88yy.js → index-pxn30mse.js} +8 -8
- package/dist/{index-d34f6r87.js → index-q361cg2f.js} +1 -1
- package/dist/{index-9j4zwcht.js → index-rvr00sg7.js} +168 -675
- package/dist/{index-qeb82z37.js → index-vdygbq10.js} +1 -1
- package/dist/{keepalive-w4aakbyk.js → keepalive-w56f32cg.js} +4 -4
- package/dist/lead-enq35nqp.js +70 -0
- package/dist/{maintenance-hbptpk6p.js → maintenance-7mdgxcea.js} +1 -1
- package/dist/{oauth-refresh-sweep-217semrv.js → oauth-refresh-sweep-29txy9vk.js} +3 -3
- package/dist/{onboard-vz2s5df3.js → onboard-kpsrkarj.js} +5 -5
- package/dist/{opencode-adapter-gtfd7d7v.js → opencode-adapter-j25vm3rr.js} +5 -5
- package/dist/{otel-impl-aefave37.js → otel-impl-6kkct7a8.js} +2 -2
- package/dist/{pi-mono-adapter-f8h5yseh.js → pi-mono-adapter-exhkzhn2.js} +117 -23
- package/dist/{pricing-refresh-m1yhh9ya.js → pricing-refresh-n2x7jh6k.js} +3 -3
- package/dist/{rbac-roles-rrw8twdz.js → rbac-roles-81ypx6es.js} +1 -1
- package/dist/{rbac-roles-svj72cdf.js → rbac-roles-b5htkjby.js} +2 -2
- package/dist/{render-v2-amwbnfxh.js → render-v2-qn6jmpk4.js} +3 -3
- package/dist/{resume-9cd6wf6z.js → resume-wd30wx23.js} +7 -7
- package/dist/{runtime-fp4vrx7m.js → runtime-4hsg7h60.js} +1 -1
- package/dist/{session-access-21t6yjyg.js → session-access-5qvfevhk.js} +4 -4
- package/dist/{session-source-backfill-pxbzcsc6.js → session-source-backfill-n1czavbr.js} +3 -3
- package/dist/{setup-4592tqz4.js → setup-dnb56qy5.js} +3 -3
- package/dist/{virtual-modules-gzfnpmnz.js → virtual-modules-76xxv9fs.js} +2 -2
- package/dist/{work-objects-t9tcf174.js → work-objects-4n21tfkm.js} +21 -20
- package/dist/{worker-follow-up-q32jh4yk.js → worker-follow-up-77hvzxww.js} +18 -17
- package/dist/worker-gg1576j3.js +70 -0
- package/dist/{x-b6wxntdt.js → x-vmmye1kb.js} +2 -2
- package/openapi.json +189 -32
- package/package.json +8 -7
- package/src/be/db/script-runs.ts +23 -0
- package/src/be/db/session-summaries.ts +8 -2
- package/src/be/db/session-tree.ts +174 -61
- package/src/be/db/session-visibility.ts +5 -4
- package/src/be/db/tasks/read.ts +122 -3
- package/src/be/db-client.ts +8 -5
- package/src/be/db.ts +23 -8
- package/src/be/memory/providers/sqlite-store.ts +49 -12
- package/src/be/migrations/259_session_summaries_open_tasks.sql +111 -0
- package/src/be/migrations/260_runtime_pool_cache.sql +20 -0
- package/src/be/migrations/262_session_visibility_indexes.sql +18 -0
- package/src/be/migrations/263_task_list_filter_index.sql +11 -0
- package/src/be/migrations/264_slack_thread_cover_index.sql +9 -0
- package/src/be/migrations/266_script_runs_source_task.sql +11 -0
- package/src/be/model-validation.ts +5 -1
- package/src/be/pricing-normalize.ts +7 -2
- package/src/be/runtime-pools/cache-cleanup.ts +109 -0
- package/src/be/runtime-pools/db.ts +65 -6
- package/src/be/runtime-pools/remote-controller.ts +5 -0
- package/src/be/runtime-pools/supervisor.ts +33 -2
- package/src/be/scripts/typecheck.ts +23 -0
- package/src/be/seed/schedules-seeder.ts +135 -9
- package/src/be/seed/workflows-seeder.ts +65 -1
- package/src/be/seed-pricing.ts +23 -0
- package/src/be/seed-scripts/catalog/task-failure-audit.ts +5 -1
- package/src/be/steering.ts +50 -3
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/commands/controller.ts +4 -0
- package/src/commands/entrypoint-api-readiness.ts +74 -0
- package/src/commands/provider-credentials.ts +29 -2
- package/src/commands/runner.ts +86 -5
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +27 -1
- package/src/extensions/dispatcher.ts +4 -0
- package/src/http/agents.ts +1 -0
- package/src/http/index.ts +1 -1
- package/src/http/list-total-cache.ts +14 -7
- package/src/http/mcp-bridge.ts +4 -0
- package/src/http/memory.ts +38 -9
- package/src/http/script-runs.ts +174 -5
- package/src/http/status.ts +10 -2
- package/src/http/tasks.ts +4 -2
- package/src/integrations/chat-sdk/inbound.ts +15 -5
- package/src/integrations/chat-sdk/steering.ts +3 -1
- package/src/kv-reserved-namespaces.ts +2 -0
- package/src/prompts/base-prompt.ts +4 -1
- package/src/prompts/session-templates.ts +26 -0
- package/src/providers/acp-adapter.ts +190 -26
- package/src/providers/acp-targets.ts +306 -6
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +9 -44
- package/src/providers/codex-oauth/flow.ts +24 -3
- package/src/providers/codex-oauth/storage.ts +56 -17
- package/src/providers/codex-oauth/subscription.ts +67 -0
- package/src/providers/fx-adapter.ts +211 -0
- package/src/providers/index.ts +5 -1
- package/src/providers/mcp-loopback-proxy.ts +94 -0
- package/src/providers/pi-mono-adapter.ts +98 -13
- package/src/providers/pricing-sources.md +8 -0
- package/src/providers/reasoning-effort.ts +17 -1
- package/src/providers/types.ts +2 -1
- package/src/runtime-providers/controller-protocol.ts +14 -1
- package/src/runtime-providers/controller.ts +5 -0
- package/src/runtime-providers/docker.ts +108 -23
- package/src/runtime-providers/types.ts +16 -0
- package/src/scheduler/scheduler.ts +36 -3
- package/src/script-workflows/workflow-ctx.ts +8 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +27 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +33 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -0
- package/src/server-user.ts +1 -0
- package/src/server.ts +7 -0
- package/src/slack/assistant.ts +17 -1
- package/src/slack/commands.ts +5 -1
- package/src/slack/handlers.ts +53 -5
- package/src/slack/left-threads.ts +88 -0
- package/src/slack/steering.ts +3 -0
- package/src/slack/thread-buffer.ts +40 -16
- package/src/tasks/follow-up-redelegation-guard.ts +155 -0
- package/src/tests/acp-adapter.test.ts +79 -0
- package/src/tests/base-prompt.test.ts +12 -0
- package/src/tests/chat-sdk-inbound.test.ts +30 -0
- package/src/tests/chatgpt-subscription-model-groups.test.ts +62 -0
- package/src/tests/chatgpt-subscription-pi.test.ts +170 -0
- package/src/tests/chatgpt-subscription-session-bench.test.ts +198 -0
- package/src/tests/claude-artifacts-read-script.test.ts +209 -0
- package/src/tests/codex-oauth-credential-info.test.ts +40 -0
- package/src/tests/codex-oauth-refresh-egress.test.ts +194 -0
- package/src/tests/context-window.test.ts +7 -0
- package/src/tests/db-client.test.ts +21 -0
- package/src/tests/db-task-read-contract.test.ts +8 -2
- package/src/tests/db-task-write-contract.test.ts +5 -1
- package/src/tests/entrypoint-api-readiness.test.ts +416 -210
- package/src/tests/explicit-model-validation.test.ts +113 -8
- package/src/tests/extensions-run-context.test.ts +11 -0
- package/src/tests/extensions-steering.test.ts +353 -0
- package/src/tests/extensions-validate.test.ts +5 -0
- package/src/tests/fixtures/extensions/steering-policy/hooks.ts +39 -0
- package/src/tests/fixtures/extensions/steering-policy/manifest.json +7 -0
- package/src/tests/fixtures/fake-fx.ts +167 -0
- package/src/tests/fixtures/fx/acp-responses-recorded.json +157 -0
- package/src/tests/fixtures/fx/acp-updates-recorded.jsonl +15 -0
- package/src/tests/follow-up-redelivery-guard.test.ts +432 -202
- package/src/tests/fx-adapter.test.ts +771 -0
- package/src/tests/grok-adapter.test.ts +36 -0
- package/src/tests/list-total-cache.test.ts +27 -2
- package/src/tests/mcp-loopback-proxy.test.ts +193 -0
- package/src/tests/memory-fts-populate.test.ts +11 -9
- package/src/tests/memory-retrieval-purge.test.ts +80 -0
- package/src/tests/migration-260-runtime-pool-cache.test.ts +118 -0
- package/src/tests/model-tier-resolution.test.ts +3 -3
- package/src/tests/pricing-refresh.test.ts +5 -5
- package/src/tests/prompt-template-session.test.ts +5 -3
- package/src/tests/provider-registration.test.ts +2 -1
- package/src/tests/query-audit-plans.test.ts +30 -2
- package/src/tests/runtime-cache-cleanup.test.ts +367 -0
- package/src/tests/runtime-e2b-pools.test.ts +41 -0
- package/src/tests/runtime-pools-db.test.ts +98 -0
- package/src/tests/runtime-pools-http.test.ts +29 -0
- package/src/tests/runtime-providers-docker.test.ts +253 -0
- package/src/tests/runtime-remote-controller.test.ts +66 -2
- package/src/tests/runtime-supervisor.test.ts +66 -2
- package/src/tests/script-connections.test.ts +13 -0
- package/src/tests/script-runs-http.test.ts +684 -2
- package/src/tests/seed-model-profiles.test.ts +2 -2
- package/src/tests/seed-schedules.test.ts +138 -0
- package/src/tests/seed-scripts.test.ts +26 -0
- package/src/tests/session-cost-recompute-unit.test.ts +39 -0
- package/src/tests/session-costs-golden.test.ts +26 -0
- package/src/tests/session-summaries-plans.test.ts +120 -0
- package/src/tests/session-summaries.test.ts +72 -12
- package/src/tests/sessions-tree.test.ts +11 -3
- package/src/tests/slack-leave-thread.test.ts +571 -0
- package/src/tests/slack-render-v2-query-plans.test.ts +24 -2
- package/src/tests/status.test.ts +11 -0
- package/src/tests/tasks-readable-row.test.ts +155 -0
- package/src/tests/tool-annotations.test.ts +2 -1
- package/src/tests/workflow-engine-v2.test.ts +42 -0
- package/src/tests/workflow-script-principal.test.ts +207 -7
- package/src/tools/send-task.ts +35 -47
- package/src/tools/slack-leave-thread.ts +104 -0
- package/src/tools/steer-task.ts +3 -2
- package/src/tools/task-dedup.ts +45 -21
- package/src/tools/tool-config.ts +2 -1
- package/src/types.ts +57 -0
- package/src/utils/codex-auth-failure.ts +30 -0
- package/src/utils/context-window.ts +4 -1
- package/src/utils/credentials.ts +7 -0
- package/src/utils/fx-models.ts +43 -0
- package/src/workflows/cooldown.ts +3 -2
- package/src/workflows/engine.ts +24 -9
- package/templates/schedules/dreaming-daily/config.json +1 -1
- package/templates/schedules/dreaming-daily/content.md +6 -14
- package/templates/skills/composio-google-docs/content.md +10 -0
- package/templates/skills/script-workflows/content.md +19 -1
- package/templates/skills/swarm-docs/content.md +2 -0
- package/templates/skills/swarm-scripts/SKILL.md +2 -0
- package/templates/skills/swarm-scripts/content.md +2 -0
- package/templates/workflows/dreaming/content.md +5 -1
- package/dist/acp-adapter-96dc64m2.js +0 -15
- package/dist/cli-dznrgaep.js +0 -30
- package/dist/cli-w3hcrxht.js +0 -6985
- package/dist/handlers-jg7v74ty.js +0 -31
- package/dist/index-8c1zbz59.js +0 -29
- package/dist/index-8k8tmfxd.js +0 -32
- package/dist/index-hq617xdj.js +0 -36
- package/dist/lead-567kw7x3.js +0 -68
- package/dist/worker-fjdqra6q.js +0 -68
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { closeDb, getAllTasks, getDb, getDbClient, initDb } from "../be/db";
|
|
4
|
+
import { readableSessionCondition } from "../be/db/tasks/read";
|
|
5
|
+
|
|
6
|
+
const TEST_DB_PATH = "./test-tasks-readable-row.sqlite";
|
|
7
|
+
const AT = "2026-10-01T00:00:00.000Z";
|
|
8
|
+
|
|
9
|
+
async function removeDbFiles(): Promise<void> {
|
|
10
|
+
for (const suffix of ["", "-wal", "-shm"])
|
|
11
|
+
await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => undefined);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function task(
|
|
15
|
+
id: string,
|
|
16
|
+
opts: { parent?: string; owner?: string; scheduleId?: string } = {},
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
await getDbClient().run(
|
|
19
|
+
`INSERT INTO agent_tasks (id, task, status, source, parentTaskId, requestedByUserId, scheduleId,
|
|
20
|
+
createdAt, lastUpdatedAt)
|
|
21
|
+
VALUES (?, ?, 'completed', 'api', ?, ?, ?, ?, ?)`,
|
|
22
|
+
[id, `task ${id}`, opts.parent ?? null, opts.owner ?? null, opts.scheduleId ?? null, AT, AT],
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("readableSessionRowCondition", () => {
|
|
27
|
+
beforeAll(async () => {
|
|
28
|
+
await removeDbFiles();
|
|
29
|
+
initDb(TEST_DB_PATH);
|
|
30
|
+
for (const id of ["u1", "u2", "u3"]) {
|
|
31
|
+
await getDbClient().run(
|
|
32
|
+
"INSERT INTO users (id, name, role, status, createdAt, lastUpdatedAt) VALUES (?, ?, 'member', 'active', ?, ?)",
|
|
33
|
+
[id, id, AT, AT],
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
// u1's session, three deep.
|
|
37
|
+
await task("a", { owner: "u1" });
|
|
38
|
+
await task("a1", { parent: "a", owner: "u1" });
|
|
39
|
+
await task("a2", { parent: "a1" });
|
|
40
|
+
// u2's session, with a child that names u1.
|
|
41
|
+
await task("b", { owner: "u2" });
|
|
42
|
+
await task("b1", { parent: "b", owner: "u1" });
|
|
43
|
+
// u2's schedule run: everyone reads it.
|
|
44
|
+
await task("c", { owner: "u2", scheduleId: "schedule-1" });
|
|
45
|
+
await task("c1", { parent: "c" });
|
|
46
|
+
// u2's session shared with u1, and one shared with everyone.
|
|
47
|
+
await task("d", { owner: "u2" });
|
|
48
|
+
await task("d1", { parent: "d" });
|
|
49
|
+
await task("e", { owner: "u2" });
|
|
50
|
+
await getDbClient().run(
|
|
51
|
+
`INSERT INTO session_shares (id, rootTaskId, userId, access, createdAt)
|
|
52
|
+
VALUES ('share-d', 'd', 'u1', 'read', ?), ('share-e', 'e', NULL, 'read', ?)`,
|
|
53
|
+
[AT, AT],
|
|
54
|
+
);
|
|
55
|
+
// A revoked share hides again.
|
|
56
|
+
await task("f", { owner: "u2" });
|
|
57
|
+
await getDbClient().run(
|
|
58
|
+
`INSERT INTO session_shares (id, rootTaskId, userId, access, createdAt, revokedAt)
|
|
59
|
+
VALUES ('share-f', 'f', 'u1', 'read', ?, ?)`,
|
|
60
|
+
[AT, AT],
|
|
61
|
+
);
|
|
62
|
+
// A session nobody owns.
|
|
63
|
+
await task("g");
|
|
64
|
+
await task("g1", { parent: "g", owner: "u2" });
|
|
65
|
+
// Orphan subtrees: the top's parent is gone, so the top is the root.
|
|
66
|
+
await task("h", { owner: "u2" });
|
|
67
|
+
await task("h1", { parent: "h" });
|
|
68
|
+
await task("h2", { parent: "h1", owner: "u1" });
|
|
69
|
+
await task("i", { owner: "u1" });
|
|
70
|
+
await task("i1", { parent: "i", owner: "u2" });
|
|
71
|
+
await task("i2", { parent: "i1" });
|
|
72
|
+
// Chains deeper than the walk's hop limit fall back to the set form.
|
|
73
|
+
for (const [root, owner] of [
|
|
74
|
+
["deep-u1", "u1"],
|
|
75
|
+
["deep-u2", "u2"],
|
|
76
|
+
] as const) {
|
|
77
|
+
await task(root, { owner });
|
|
78
|
+
let parent: string = root;
|
|
79
|
+
for (let i = 1; i <= 70; i++) {
|
|
80
|
+
await task(`${root}-${i}`, { parent });
|
|
81
|
+
parent = `${root}-${i}`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
getDb().run("PRAGMA foreign_keys = OFF");
|
|
85
|
+
getDb().run("DELETE FROM agent_tasks WHERE id IN ('h', 'i')");
|
|
86
|
+
getDb().run("PRAGMA foreign_keys = ON");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
afterAll(async () => {
|
|
90
|
+
closeDb();
|
|
91
|
+
await removeDbFiles();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
for (const userId of ["u1", "u2", "u3", null]) {
|
|
95
|
+
test(`the page shows the rows the set form shows, for ${userId ?? "a guest"}`, async () => {
|
|
96
|
+
const set = readableSessionCondition(userId);
|
|
97
|
+
const expected = (
|
|
98
|
+
await getDbClient().query<{ id: string }>(
|
|
99
|
+
`SELECT id FROM agent_tasks WHERE ${set.sql} ORDER BY id`,
|
|
100
|
+
set.params,
|
|
101
|
+
)
|
|
102
|
+
).map((row) => row.id);
|
|
103
|
+
const page = await getAllTasks({ readableBy: { userId }, limit: 1000 });
|
|
104
|
+
expect(page.map((t) => t.id).sort()).toEqual(expected);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
test("u1 sees exactly the expected sessions", async () => {
|
|
109
|
+
const page = await getAllTasks({ readableBy: { userId: "u1" }, limit: 1000 });
|
|
110
|
+
const deep = Array.from({ length: 70 }, (_, i) => `deep-u1-${i + 1}`);
|
|
111
|
+
expect(page.map((t) => t.id).sort()).toEqual(
|
|
112
|
+
[
|
|
113
|
+
"a",
|
|
114
|
+
"a1",
|
|
115
|
+
"a2",
|
|
116
|
+
"c",
|
|
117
|
+
"c1",
|
|
118
|
+
"d",
|
|
119
|
+
"d1",
|
|
120
|
+
"e",
|
|
121
|
+
"g",
|
|
122
|
+
"g1",
|
|
123
|
+
"h1",
|
|
124
|
+
"h2",
|
|
125
|
+
"deep-u1",
|
|
126
|
+
...deep,
|
|
127
|
+
].sort(),
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("the page walks up from each row it reads", async () => {
|
|
132
|
+
const query = spyOn(getDbClient(), "query");
|
|
133
|
+
let sql = "";
|
|
134
|
+
let params: unknown[] = [];
|
|
135
|
+
try {
|
|
136
|
+
await getAllTasks({ status: "completed", readableBy: { userId: "u1" } });
|
|
137
|
+
[sql, params = []] = query.mock.calls.find(([s]) => s.includes("WITH page AS"))! as [
|
|
138
|
+
string,
|
|
139
|
+
unknown[]?,
|
|
140
|
+
];
|
|
141
|
+
} finally {
|
|
142
|
+
query.mockRestore();
|
|
143
|
+
}
|
|
144
|
+
const plan = getDb()
|
|
145
|
+
.query<{ detail: string }, any[]>(`EXPLAIN QUERY PLAN ${sql}`)
|
|
146
|
+
.all(...(params as any[]))
|
|
147
|
+
.map((row) => row.detail)
|
|
148
|
+
.join("\n");
|
|
149
|
+
// The walk is a correlated lookup per row. The set form appears only as
|
|
150
|
+
// the fallback branch, which SQLite builds when a walk finds no root.
|
|
151
|
+
expect(plan).toContain("CORRELATED SCALAR SUBQUERY");
|
|
152
|
+
expect(plan).toContain("SEARCH u USING INDEX sqlite_autoindex_agent_tasks_1 (id=?)");
|
|
153
|
+
expect(plan).toContain("SEARCH p USING INDEX sqlite_autoindex_agent_tasks_1 (id=?)");
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -181,6 +181,7 @@ describe("Tool Annotations & Classification", () => {
|
|
|
181
181
|
"slack-list-channels",
|
|
182
182
|
"slack-delete",
|
|
183
183
|
"slack-update",
|
|
184
|
+
"slack-leave-thread",
|
|
184
185
|
];
|
|
185
186
|
|
|
186
187
|
for (const name of slackTools) {
|
|
@@ -379,7 +380,7 @@ describe("Tool Annotations & Classification", () => {
|
|
|
379
380
|
// the six Chat SDK tools (chat-* + register/unregister-chat-channel),
|
|
380
381
|
// and the seven app row tools plus app-action-run.
|
|
381
382
|
expect(count).toBeGreaterThanOrEqual(45);
|
|
382
|
-
expect(count).toBeLessThanOrEqual(
|
|
383
|
+
expect(count).toBeLessThanOrEqual(170);
|
|
383
384
|
});
|
|
384
385
|
|
|
385
386
|
test("core tools are fewer than deferred tools", () => {
|
|
@@ -945,6 +945,48 @@ describe("Workflow Engine v2 (Phase 3)", () => {
|
|
|
945
945
|
expect(run2!.status).toBe("skipped");
|
|
946
946
|
expect(run2!.error).toBe("cooldown");
|
|
947
947
|
});
|
|
948
|
+
|
|
949
|
+
test("simultaneous triggers start one run; the other records a cooldown skip", async () => {
|
|
950
|
+
const registry = createTestRegistry();
|
|
951
|
+
const def: WorkflowDefinition = {
|
|
952
|
+
nodes: [{ id: "a", type: "echo", config: { message: "hi" } }],
|
|
953
|
+
};
|
|
954
|
+
const workflow = await makeWorkflow(def, { cooldown: { hours: 20, from: "start" } });
|
|
955
|
+
|
|
956
|
+
// Both pass the pre-transaction check before either run row exists.
|
|
957
|
+
const runIds = await Promise.all([
|
|
958
|
+
startWorkflowExecution(workflow, {}, registry),
|
|
959
|
+
startWorkflowExecution(workflow, {}, registry),
|
|
960
|
+
]);
|
|
961
|
+
const statuses = await Promise.all(
|
|
962
|
+
runIds.map(async (id) => (await getWorkflowRun(id))!.status),
|
|
963
|
+
);
|
|
964
|
+
expect(statuses.sort()).toEqual(["completed", "skipped"]);
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
test('from: "start" ignores a long successful run that finished inside the window', async () => {
|
|
968
|
+
const { createWorkflowRun, updateWorkflowRun } = await import("../be/db");
|
|
969
|
+
const def: WorkflowDefinition = {
|
|
970
|
+
nodes: [{ id: "a", type: "echo", config: {} }],
|
|
971
|
+
};
|
|
972
|
+
const workflow = await makeWorkflow(def);
|
|
973
|
+
|
|
974
|
+
// Started 24h ago, finished 1h ago: 24h since its start, 1h since its finish.
|
|
975
|
+
const runId = crypto.randomUUID();
|
|
976
|
+
await createWorkflowRun({ id: runId, workflowId: workflow.id });
|
|
977
|
+
await getDbClient().run("UPDATE workflow_runs SET startedAt = ? WHERE id = ?", [
|
|
978
|
+
new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
|
|
979
|
+
runId,
|
|
980
|
+
]);
|
|
981
|
+
await updateWorkflowRun(runId, {
|
|
982
|
+
status: "completed",
|
|
983
|
+
finishedAt: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
expect(await shouldSkipCooldown(workflow.id, { hours: 20, from: "start" })).toBe(false);
|
|
987
|
+
// Control: the default also counts from the successful finish.
|
|
988
|
+
expect(await shouldSkipCooldown(workflow.id, { hours: 20 })).toBe(true);
|
|
989
|
+
});
|
|
948
990
|
});
|
|
949
991
|
|
|
950
992
|
// ─── Input Resolution ─────────────────────────────────────
|
|
@@ -13,14 +13,19 @@ import {
|
|
|
13
13
|
createAgent,
|
|
14
14
|
createScheduledTask,
|
|
15
15
|
createWorkflow,
|
|
16
|
+
createWorkflowRun,
|
|
16
17
|
getDbClient,
|
|
18
|
+
getScheduledTaskByName,
|
|
19
|
+
getWorkflowRun,
|
|
17
20
|
initDb,
|
|
18
21
|
listWorkflows,
|
|
22
|
+
updateWorkflow,
|
|
19
23
|
} from "../be/db";
|
|
20
24
|
import { upsertScriptByName } from "../be/scripts/db";
|
|
21
25
|
import { setScriptEmbeddingProviderForTests } from "../be/scripts/embeddings";
|
|
22
26
|
import * as runCredential from "../be/scripts/run-credential";
|
|
23
27
|
import { runSeeder } from "../be/seed";
|
|
28
|
+
import { createSchedulesSeeder } from "../be/seed/schedules-seeder";
|
|
24
29
|
import { createWorkflowsSeeder } from "../be/seed/workflows-seeder";
|
|
25
30
|
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
26
31
|
import { handleWorkflows } from "../http/workflows";
|
|
@@ -34,6 +39,7 @@ import {
|
|
|
34
39
|
workflowEventBus,
|
|
35
40
|
} from "../workflows";
|
|
36
41
|
import { handleEventTrigger } from "../workflows/triggers";
|
|
42
|
+
import { snapshotAndUpdateWorkflow } from "../workflows/version";
|
|
37
43
|
import { listenOnFreePort } from "./test-net";
|
|
38
44
|
|
|
39
45
|
// A creator-less workflow (the seeded `dreaming`) runs its `swarm-script` nodes as
|
|
@@ -160,9 +166,31 @@ afterAll(async () => {
|
|
|
160
166
|
await removeDbFiles();
|
|
161
167
|
});
|
|
162
168
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
169
|
+
/** Put the seeded dreaming row back after a test edits it. */
|
|
170
|
+
async function restoreDreaming(): Promise<void> {
|
|
171
|
+
await updateWorkflow(dreaming.id, {
|
|
172
|
+
description: dreaming.description,
|
|
173
|
+
definition: dreaming.definition,
|
|
174
|
+
input: dreaming.input,
|
|
175
|
+
cooldown: dreaming.cooldown,
|
|
176
|
+
enabled: dreaming.enabled,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Move every dreaming run start out of its 20-hour cooldown window. */
|
|
181
|
+
async function ageDreamingRuns(): Promise<void> {
|
|
182
|
+
await getDbClient().run(
|
|
183
|
+
`UPDATE workflow_runs SET startedAt = '2000-01-01T00:00:00.000Z',
|
|
184
|
+
finishedAt = CASE WHEN finishedAt IS NULL THEN NULL ELSE '2000-01-01T00:00:00.000Z' END
|
|
185
|
+
WHERE workflowId = ?`,
|
|
186
|
+
[dreaming.id],
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
beforeEach(async () => {
|
|
191
|
+
// Dreaming has a 20-hour cooldown. Age earlier runs (parked on an agent-task)
|
|
192
|
+
// so they do not block the next trigger.
|
|
193
|
+
await ageDreamingRuns();
|
|
166
194
|
calls = [];
|
|
167
195
|
scriptResult = QUIET_GATHER;
|
|
168
196
|
});
|
|
@@ -241,6 +269,179 @@ describe("swarm-script principal for a creator-less workflow", () => {
|
|
|
241
269
|
expect(gatherPrincipals()).toEqual([leadId]);
|
|
242
270
|
});
|
|
243
271
|
|
|
272
|
+
test("a creator-less schedule runs as the lead, resolved at fire time", async () => {
|
|
273
|
+
const schedule = await createScheduledTask({
|
|
274
|
+
name: `principal-seeded-${crypto.randomUUID().slice(0, 8)}`,
|
|
275
|
+
cronExpression: "0 0 1 1 *",
|
|
276
|
+
targetType: "workflow",
|
|
277
|
+
workflowId: dreaming.id,
|
|
278
|
+
targetAgentId: workerId,
|
|
279
|
+
});
|
|
280
|
+
expect(schedule.createdByAgentId).toBeUndefined();
|
|
281
|
+
await dispatchScheduleTarget(schedule);
|
|
282
|
+
expect(gatherPrincipals()).toEqual([leadId]);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("a creator-less schedule fails its fire when the swarm has no lead", async () => {
|
|
286
|
+
const schedule = await createScheduledTask({
|
|
287
|
+
name: `principal-leadless-${crypto.randomUUID().slice(0, 8)}`,
|
|
288
|
+
cronExpression: "0 0 1 1 *",
|
|
289
|
+
targetType: "workflow",
|
|
290
|
+
workflowId: dreaming.id,
|
|
291
|
+
});
|
|
292
|
+
await getDbClient().run("UPDATE agents SET isLead = 0 WHERE id = ?", [leadId]);
|
|
293
|
+
try {
|
|
294
|
+
await expect(dispatchScheduleTarget(schedule)).rejects.toThrow(
|
|
295
|
+
"has no creator and the swarm has no lead",
|
|
296
|
+
);
|
|
297
|
+
} finally {
|
|
298
|
+
await getDbClient().run("UPDATE agents SET isLead = 1 WHERE id = ?", [leadId]);
|
|
299
|
+
}
|
|
300
|
+
expect(calls).toEqual([]);
|
|
301
|
+
const runs = await getDbClient().query<{ id: string }>(
|
|
302
|
+
"SELECT id FROM workflow_runs WHERE workflowId = ? AND startedAt > '2001-01-01'",
|
|
303
|
+
[dreaming.id],
|
|
304
|
+
);
|
|
305
|
+
expect(runs).toEqual([]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("a creator-less schedule keeps the lead after the documented input setup and enable", async () => {
|
|
309
|
+
// The guide's setup: update-workflow with every input key, then turn it on.
|
|
310
|
+
// Both snapshot the seeded state first, as the tool and REST paths do.
|
|
311
|
+
await snapshotAndUpdateWorkflow(
|
|
312
|
+
dreaming.id,
|
|
313
|
+
{ input: { ...dreaming.input, leadAgentId: leadId, slackChannel: "C123" } },
|
|
314
|
+
{ changedByAgentId: workerId },
|
|
315
|
+
);
|
|
316
|
+
await snapshotAndUpdateWorkflow(dreaming.id, { enabled: false });
|
|
317
|
+
await snapshotAndUpdateWorkflow(dreaming.id, {
|
|
318
|
+
enabled: true,
|
|
319
|
+
description: "tuned by the operator",
|
|
320
|
+
cooldown: { hours: 22, from: "start" },
|
|
321
|
+
});
|
|
322
|
+
const schedule = await createScheduledTask({
|
|
323
|
+
name: `principal-tuned-${crypto.randomUUID().slice(0, 8)}`,
|
|
324
|
+
cronExpression: "0 0 1 1 *",
|
|
325
|
+
targetType: "workflow",
|
|
326
|
+
workflowId: dreaming.id,
|
|
327
|
+
});
|
|
328
|
+
try {
|
|
329
|
+
await dispatchScheduleTarget(schedule);
|
|
330
|
+
expect(gatherPrincipals()).toEqual([leadId]);
|
|
331
|
+
expect(calls[0]?.args.leadAgentId).toBe(leadId);
|
|
332
|
+
} finally {
|
|
333
|
+
await restoreDreaming();
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("a creator-less schedule refuses the lead once a worker edits the seeded nodes", async () => {
|
|
338
|
+
const schedule = await createScheduledTask({
|
|
339
|
+
name: `principal-edited-${crypto.randomUUID().slice(0, 8)}`,
|
|
340
|
+
cronExpression: "0 0 1 1 *",
|
|
341
|
+
targetType: "workflow",
|
|
342
|
+
workflowId: dreaming.id,
|
|
343
|
+
});
|
|
344
|
+
const [gather, ...rest] = dreaming.definition.nodes;
|
|
345
|
+
await snapshotAndUpdateWorkflow(
|
|
346
|
+
dreaming.id,
|
|
347
|
+
{
|
|
348
|
+
input: { ...dreaming.input, leadAgentId: leadId },
|
|
349
|
+
definition: {
|
|
350
|
+
...dreaming.definition,
|
|
351
|
+
nodes: [{ ...gather!, config: { ...gather!.config, scriptName: "evil" } }, ...rest],
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
{ changedByAgentId: workerId },
|
|
355
|
+
);
|
|
356
|
+
try {
|
|
357
|
+
await expect(dispatchScheduleTarget(schedule)).rejects.toThrow(
|
|
358
|
+
'the nodes or params of seeded workflow "dreaming" were edited',
|
|
359
|
+
);
|
|
360
|
+
} finally {
|
|
361
|
+
await restoreDreaming();
|
|
362
|
+
}
|
|
363
|
+
expect(calls).toEqual([]);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("a creator-less schedule refuses the lead for an edit with no seeded snapshot", async () => {
|
|
367
|
+
const schedule = await createScheduledTask({
|
|
368
|
+
name: `principal-unsnapshotted-${crypto.randomUUID().slice(0, 8)}`,
|
|
369
|
+
cronExpression: "0 0 1 1 *",
|
|
370
|
+
targetType: "workflow",
|
|
371
|
+
workflowId: dreaming.id,
|
|
372
|
+
});
|
|
373
|
+
// Delete the history so no snapshot proves the seeded state.
|
|
374
|
+
await getDbClient().run("DELETE FROM workflow_versions WHERE workflowId = ?", [dreaming.id]);
|
|
375
|
+
await updateWorkflow(dreaming.id, { description: "edited without a snapshot" });
|
|
376
|
+
try {
|
|
377
|
+
await expect(dispatchScheduleTarget(schedule)).rejects.toThrow("were edited");
|
|
378
|
+
} finally {
|
|
379
|
+
await restoreDreaming();
|
|
380
|
+
}
|
|
381
|
+
expect(calls).toEqual([]);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("a long successful night does not make the next nightly fire skip", async () => {
|
|
385
|
+
const schedule = await createScheduledTask({
|
|
386
|
+
name: `principal-long-${crypto.randomUUID().slice(0, 8)}`,
|
|
387
|
+
cronExpression: "0 0 1 1 *",
|
|
388
|
+
targetType: "workflow",
|
|
389
|
+
workflowId: dreaming.id,
|
|
390
|
+
});
|
|
391
|
+
// Last night started 24h ago and finished 1h ago.
|
|
392
|
+
const runId = crypto.randomUUID();
|
|
393
|
+
await createWorkflowRun({ id: runId, workflowId: dreaming.id });
|
|
394
|
+
await getDbClient().run(
|
|
395
|
+
"UPDATE workflow_runs SET status = 'completed', startedAt = ?, finishedAt = ? WHERE id = ?",
|
|
396
|
+
[
|
|
397
|
+
new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
|
|
398
|
+
new Date(Date.now() - 60 * 60 * 1000).toISOString(),
|
|
399
|
+
runId,
|
|
400
|
+
],
|
|
401
|
+
);
|
|
402
|
+
const fired = await dispatchScheduleTarget(schedule);
|
|
403
|
+
const run = await getWorkflowRun(fired.workflowRunIds![0]!);
|
|
404
|
+
expect(run?.status).not.toBe("skipped");
|
|
405
|
+
expect(gatherPrincipals()).toEqual([leadId]);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("control: a creator-less workflow the seeder never wrote keeps running agentless", async () => {
|
|
409
|
+
const copy = await createWorkflow({
|
|
410
|
+
name: `principal-unseeded-${crypto.randomUUID()}`,
|
|
411
|
+
definition: dreaming.definition,
|
|
412
|
+
});
|
|
413
|
+
const schedule = await createScheduledTask({
|
|
414
|
+
name: `principal-unseeded-${crypto.randomUUID().slice(0, 8)}`,
|
|
415
|
+
cronExpression: "0 0 1 1 *",
|
|
416
|
+
targetType: "workflow",
|
|
417
|
+
workflowId: copy.id,
|
|
418
|
+
});
|
|
419
|
+
await dispatchScheduleTarget(schedule);
|
|
420
|
+
expect(gatherPrincipals()).toEqual(["workflow"]);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("end to end: the seeded dreaming-daily schedule fires dreaming as the lead, once a day", async () => {
|
|
424
|
+
const seeded = await runSeeder(createSchedulesSeeder(), { quiet: true });
|
|
425
|
+
expect(seeded.failed).toEqual([]);
|
|
426
|
+
const schedule = await getScheduledTaskByName("dreaming-daily");
|
|
427
|
+
expect(schedule).toMatchObject({ targetType: "workflow", workflowId: dreaming.id });
|
|
428
|
+
expect(schedule?.taskTemplate).toBeUndefined();
|
|
429
|
+
expect(schedule?.createdByAgentId).toBeUndefined();
|
|
430
|
+
|
|
431
|
+
const first = await dispatchScheduleTarget(schedule!);
|
|
432
|
+
expect(first.task).toBeUndefined();
|
|
433
|
+
expect(first.workflowRunIds).toHaveLength(1);
|
|
434
|
+
expect(gatherPrincipals()).toEqual([leadId]);
|
|
435
|
+
const run = await getWorkflowRun(first.workflowRunIds![0]!);
|
|
436
|
+
expect(run?.triggerData).toMatchObject({ scheduleName: "dreaming-daily" });
|
|
437
|
+
|
|
438
|
+
// The cooldown replaces the old task prompt's 20-hour double-run guard.
|
|
439
|
+
const second = await dispatchScheduleTarget(schedule!);
|
|
440
|
+
const skipped = await getWorkflowRun(second.workflowRunIds![0]!);
|
|
441
|
+
expect(skipped).toMatchObject({ status: "skipped", error: "cooldown" });
|
|
442
|
+
expect(gatherPrincipals()).toEqual([leadId]);
|
|
443
|
+
});
|
|
444
|
+
|
|
244
445
|
test("control: a workflow with a creator still runs as its creator", async () => {
|
|
245
446
|
const owned = await createWorkflow({
|
|
246
447
|
name: `principal-owned-${crypto.randomUUID()}`,
|
|
@@ -251,10 +452,9 @@ describe("swarm-script principal for a creator-less workflow", () => {
|
|
|
251
452
|
expect(gatherPrincipals()).toEqual([leadId]);
|
|
252
453
|
});
|
|
253
454
|
|
|
254
|
-
test("control:
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
// synthesize and apply-lead target gather's leadAgentId.
|
|
455
|
+
test("control: a worker's manual trigger with no data runs the *-lead nodes on the lead", async () => {
|
|
456
|
+
// A worker that calls trigger-workflow with no triggerData. Scripts run as
|
|
457
|
+
// that worker, while synthesize and apply-lead target gather's leadAgentId.
|
|
258
458
|
scriptResult = {
|
|
259
459
|
ok: true,
|
|
260
460
|
agents: [{ id: workerId, name: "principal-worker" }],
|
package/src/tools/send-task.ts
CHANGED
|
@@ -5,9 +5,7 @@ import { resolveTaskAuditUserId } from "@/be/audit-user";
|
|
|
5
5
|
import {
|
|
6
6
|
createTaskExtended,
|
|
7
7
|
extensionAgentAssignmentError,
|
|
8
|
-
findCompletedTaskInThread,
|
|
9
8
|
findExistingLinearTrackerContextWork,
|
|
10
|
-
findRecentCancelledTaskInThread,
|
|
11
9
|
getActiveTaskCount,
|
|
12
10
|
getAgentById,
|
|
13
11
|
getDbClient,
|
|
@@ -23,6 +21,10 @@ import { repointTrackerSyncBySwarmId } from "@/be/db-queries/tracker";
|
|
|
23
21
|
import { explicitModelErrorForAgent } from "@/be/model-validation";
|
|
24
22
|
import { applyPreTaskCreate } from "@/extensions/apply-task-create";
|
|
25
23
|
import { can } from "@/rbac";
|
|
24
|
+
import {
|
|
25
|
+
evaluateFollowUpRedelegation,
|
|
26
|
+
followUpThreadForSourceTask,
|
|
27
|
+
} from "@/tasks/follow-up-redelegation-guard";
|
|
26
28
|
import { checkSlackRoutingCoherence } from "@/tasks/slack-routing";
|
|
27
29
|
import { findDuplicateTask } from "@/tools/task-dedup";
|
|
28
30
|
import { ownerCtx, type ToolCtx } from "@/tools/task-tool-ctx";
|
|
@@ -163,7 +165,13 @@ export const sendTaskInputSchema = z
|
|
|
163
165
|
.boolean()
|
|
164
166
|
.default(false)
|
|
165
167
|
.describe(
|
|
166
|
-
"If true, skip duplicate detection and create the task even if a similar one exists.",
|
|
168
|
+
"If true, skip duplicate detection and create the task even if a similar one exists. Does not bypass the follow-up re-delegation guard; see allowRedelegation.",
|
|
169
|
+
),
|
|
170
|
+
allowRedelegation: z
|
|
171
|
+
.boolean()
|
|
172
|
+
.default(false)
|
|
173
|
+
.describe(
|
|
174
|
+
"Only matters when you call send-task from a follow-up task in a Slack thread. Such a dispatch is blocked when it repeats a task already completed in that thread (same-work rule, unless a task there was cancelled or failed after that completion), or when you already dispatched 10 tasks into the thread in the last 6 hours (loop-cap rule). Set true to dispatch anyway; the override is logged. Use it only for deliberate re-runs or long chains, never to retry a block you have not read.",
|
|
167
175
|
),
|
|
168
176
|
slackChannelId: z
|
|
169
177
|
.string()
|
|
@@ -292,6 +300,7 @@ export async function sendTaskHandler(
|
|
|
292
300
|
effort,
|
|
293
301
|
profile,
|
|
294
302
|
allowDuplicate,
|
|
303
|
+
allowRedelegation,
|
|
295
304
|
slackChannelId,
|
|
296
305
|
slackThreadTs,
|
|
297
306
|
slackUserId,
|
|
@@ -316,6 +325,7 @@ export async function sendTaskHandler(
|
|
|
316
325
|
const requestedLeadOnly = ctx.kind === "user" ? false : leadOnly;
|
|
317
326
|
const requestedCapabilities = ctx.kind === "user" ? undefined : requiredCapabilities;
|
|
318
327
|
const requestedAllowDuplicate = ctx.kind === "user" ? false : allowDuplicate;
|
|
328
|
+
const requestedAllowRedelegation = ctx.kind === "user" ? false : allowRedelegation;
|
|
319
329
|
const requestedOverrideSlackContext = ctx.kind === "user" ? false : overrideSlackContext;
|
|
320
330
|
|
|
321
331
|
// Defense in depth for direct TypeScript callers that bypass MCP schema parsing.
|
|
@@ -572,7 +582,9 @@ export async function sendTaskHandler(
|
|
|
572
582
|
// Every read in this handler releases the FIFO lock, so two concurrent
|
|
573
583
|
// send-task calls with the same creator and text otherwise both pass the
|
|
574
584
|
// guards and both create a task (no UNIQUE index arbitrates them).
|
|
575
|
-
const evaluateDedupGuards = async (
|
|
585
|
+
const evaluateDedupGuards = async (
|
|
586
|
+
logOverride: boolean,
|
|
587
|
+
): Promise<{
|
|
576
588
|
ok: boolean;
|
|
577
589
|
message: string;
|
|
578
590
|
task?: AgentTask;
|
|
@@ -602,59 +614,35 @@ export async function sendTaskHandler(
|
|
|
602
614
|
}
|
|
603
615
|
}
|
|
604
616
|
|
|
605
|
-
// Guard: prevent re-delegation from follow-up tasks
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
// This prevents the cycle: worker completes → follow-up → Lead re-delegates → repeat.
|
|
609
|
-
//
|
|
610
|
-
// Exception: if a MORE RECENT task in the same thread was cancelled (exit 130,
|
|
611
|
-
// status='cancelled', or status='failed' with failureReason containing
|
|
612
|
-
// "cancelled"), bypass the guard. A cancellation means the work was
|
|
613
|
-
// interrupted — re-dispatch is the correct response, not a deduped no-op.
|
|
614
|
-
// Without this bypass, a cancelled worker permanently jams the thread
|
|
615
|
-
// against re-delegation when an earlier completed sibling exists.
|
|
617
|
+
// Guard: prevent re-delegation from follow-up tasks (same-work and
|
|
618
|
+
// loop-cap rules, see src/tasks/follow-up-redelegation-guard.ts).
|
|
619
|
+
// allowRedelegation bypasses both and is logged.
|
|
616
620
|
//
|
|
617
621
|
// NOTE: `taskType === "resume"` (created by createResumeFollowUp on
|
|
618
622
|
// supersede) is intentionally NOT in this guard — a resume IS the legitimate
|
|
619
623
|
// re-dispatch and bypassing the check is correct. Do not add "resume" here.
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
624
|
+
const followUpThread = creatorAgentId ? await followUpThreadForSourceTask(sourceTaskId) : null;
|
|
625
|
+
if (followUpThread && creatorAgentId) {
|
|
626
|
+
const blocked = await evaluateFollowUpRedelegation({
|
|
627
|
+
...followUpThread,
|
|
628
|
+
creatorAgentId,
|
|
629
|
+
taskDescription,
|
|
630
|
+
targetAgentId: taskOptions.agentId ?? taskOptions.offeredTo,
|
|
631
|
+
});
|
|
632
|
+
if (blocked && !requestedAllowRedelegation) {
|
|
633
|
+
return { ok: false, message: blocked.message };
|
|
634
|
+
}
|
|
635
|
+
if (blocked && logOverride) {
|
|
636
|
+
console.log(
|
|
637
|
+
`[send-task] follow-up re-delegation override: rule=${blocked.rule} creatorAgentId=${creatorAgentId} sourceTaskId=${sourceTaskId} thread=${followUpThread.channelId}/${followUpThread.threadTs}`,
|
|
631
638
|
);
|
|
632
|
-
if (recentCompleted) {
|
|
633
|
-
const recentCancelled = await findRecentCancelledTaskInThread(
|
|
634
|
-
sourceTask.slackChannelId,
|
|
635
|
-
sourceTask.slackThreadTs,
|
|
636
|
-
2880,
|
|
637
|
-
);
|
|
638
|
-
const cancelledMoreRecent =
|
|
639
|
-
recentCancelled &&
|
|
640
|
-
new Date(recentCancelled.lastUpdatedAt).getTime() >
|
|
641
|
-
new Date(recentCompleted.lastUpdatedAt).getTime();
|
|
642
|
-
if (!cancelledMoreRecent) {
|
|
643
|
-
return {
|
|
644
|
-
ok: false,
|
|
645
|
-
message: `Blocked: re-delegation from follow-up task in a thread that already has completed work (task ${recentCompleted.id.slice(0, 8)}). The original request was already handled.`,
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
// else: fall through — the cancellation is more recent than the
|
|
649
|
-
// completion, so re-delegation is legitimate.
|
|
650
|
-
}
|
|
651
639
|
}
|
|
652
640
|
}
|
|
653
641
|
|
|
654
642
|
return null;
|
|
655
643
|
};
|
|
656
644
|
|
|
657
|
-
const guard = await evaluateDedupGuards();
|
|
645
|
+
const guard = await evaluateDedupGuards(true);
|
|
658
646
|
if (guard) {
|
|
659
647
|
const guardData = {
|
|
660
648
|
yourAgentId: creatorAgentId,
|
|
@@ -669,7 +657,7 @@ export async function sendTaskHandler(
|
|
|
669
657
|
// Authoritative re-check: the reads above are separated from the INSERT by
|
|
670
658
|
// the whole handler, so only a guard inside this transaction can see a
|
|
671
659
|
// concurrent send-task's committed task.
|
|
672
|
-
const raced = await evaluateDedupGuards();
|
|
660
|
+
const raced = await evaluateDedupGuards(false);
|
|
673
661
|
if (raced) return { success: raced.ok, message: raced.message, task: raced.task };
|
|
674
662
|
|
|
675
663
|
// This transaction already checked the tracker key, excluding the caller's
|