@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,571 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlinkSync } from "node:fs";
|
|
3
|
+
import { createServer, type Server } from "node:http";
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import {
|
|
7
|
+
closeDb,
|
|
8
|
+
createAgent,
|
|
9
|
+
createTaskExtended,
|
|
10
|
+
createWorkflow,
|
|
11
|
+
deleteSwarmConfig,
|
|
12
|
+
getKv,
|
|
13
|
+
getSlackTasksInThread,
|
|
14
|
+
initDb,
|
|
15
|
+
updateWorkflow,
|
|
16
|
+
upsertSwarmConfig,
|
|
17
|
+
} from "../be/db";
|
|
18
|
+
import { __resetInjectedEnvTracking, loadGlobalConfigsIntoEnv } from "../http/core";
|
|
19
|
+
import { invokeToolInProcess, resetBridgeServerForTesting } from "../http/mcp-bridge";
|
|
20
|
+
import { handleTasks } from "../http/tasks";
|
|
21
|
+
import { reservedNamespaceError } from "../kv-reserved-namespaces";
|
|
22
|
+
import { createServer as createMcpServer } from "../server";
|
|
23
|
+
import { registerCommandHandler } from "../slack/commands";
|
|
24
|
+
import { registerMessageHandler, resetSlackHandlerCachesForTesting } from "../slack/handlers";
|
|
25
|
+
import {
|
|
26
|
+
isSlackThreadLeft,
|
|
27
|
+
SLACK_LEFT_THREAD_CONFIRMATION,
|
|
28
|
+
SLACK_LEFT_THREAD_KV_NAMESPACE,
|
|
29
|
+
} from "../slack/left-threads";
|
|
30
|
+
import { bufferThreadMessage, getBufferMessageCount, instantFlush } from "../slack/thread-buffer";
|
|
31
|
+
import { slackContextKey } from "../tasks/context-key";
|
|
32
|
+
import { registerSlackLeaveThreadTool } from "../tools/slack-leave-thread";
|
|
33
|
+
import type { Agent, AgentTask } from "../types";
|
|
34
|
+
import { setRequestAuth } from "../utils/request-auth-context";
|
|
35
|
+
import { workflowEventBus } from "../workflows/event-bus";
|
|
36
|
+
import { BaseExecutor, type ExecutorResult } from "../workflows/executors/base";
|
|
37
|
+
import { ExecutorRegistry } from "../workflows/executors/registry";
|
|
38
|
+
import { handleEventTrigger } from "../workflows/triggers";
|
|
39
|
+
import { listenOnFreePort } from "./test-net";
|
|
40
|
+
|
|
41
|
+
const TEST_DB_PATH = "./test-slack-leave-thread.sqlite";
|
|
42
|
+
const BOT_USER_ID = "U_LEAVE_BOT";
|
|
43
|
+
const HUMAN = "U_LEAVE_HUMAN";
|
|
44
|
+
|
|
45
|
+
const previousEnv = {
|
|
46
|
+
SLACK_RENDER_V2: process.env.SLACK_RENDER_V2,
|
|
47
|
+
ADDITIVE_SLACK: process.env.ADDITIVE_SLACK,
|
|
48
|
+
SLACK_BLOCK_EXTERNAL_CHANNELS: process.env.SLACK_BLOCK_EXTERNAL_CHANNELS,
|
|
49
|
+
SLACK_LEAVE_THREAD_DISABLE: process.env.SLACK_LEAVE_THREAD_DISABLE,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type Handler = (args: Record<string, unknown>) => Promise<void>;
|
|
53
|
+
const handlers = new Map<string, Handler>();
|
|
54
|
+
const commands = new Map<string, Handler>();
|
|
55
|
+
const ephemerals: Array<Record<string, unknown>> = [];
|
|
56
|
+
|
|
57
|
+
const client = {
|
|
58
|
+
auth: { test: async () => ({ user_id: BOT_USER_ID, bot_id: "B_LEAVE_BOT" }) },
|
|
59
|
+
conversations: {
|
|
60
|
+
info: async () => ({ ok: true, channel: {} }),
|
|
61
|
+
replies: async () => ({ ok: true, messages: [] }),
|
|
62
|
+
},
|
|
63
|
+
chat: {
|
|
64
|
+
postMessage: async () => ({ ok: true, ts: "1.1" }),
|
|
65
|
+
postEphemeral: async (args: Record<string, unknown>) => {
|
|
66
|
+
ephemerals.push(args);
|
|
67
|
+
return { ok: true };
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
reactions: { add: async () => ({ ok: true }) },
|
|
71
|
+
users: { info: async () => ({ ok: true, user: { profile: {} } }) },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
type RegisteredTool = {
|
|
75
|
+
handler: (
|
|
76
|
+
args: unknown,
|
|
77
|
+
extra: unknown,
|
|
78
|
+
) => Promise<{ structuredContent: { success: boolean; message: string } }>;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function buildTool(): RegisteredTool {
|
|
82
|
+
const server = new McpServer({ name: "slack-leave-thread-test", version: "1.0.0" });
|
|
83
|
+
registerSlackLeaveThreadTool(server);
|
|
84
|
+
const tool = (server as unknown as { _registeredTools: Record<string, RegisteredTool> })
|
|
85
|
+
._registeredTools["slack-leave-thread"];
|
|
86
|
+
if (!tool) throw new Error("slack-leave-thread tool not registered");
|
|
87
|
+
return tool;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function meta(agentId: string) {
|
|
91
|
+
return { sessionId: "s1", requestInfo: { headers: { "x-agent-id": agentId } } };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let counter = 0;
|
|
95
|
+
async function sendThreadReply(channel: string, threadTs: string, text: string) {
|
|
96
|
+
counter++;
|
|
97
|
+
await handlers.get("message")!({
|
|
98
|
+
event: {
|
|
99
|
+
channel,
|
|
100
|
+
ts: `18000.${String(counter).padStart(4, "0")}`,
|
|
101
|
+
thread_ts: threadTs,
|
|
102
|
+
text,
|
|
103
|
+
user: HUMAN,
|
|
104
|
+
},
|
|
105
|
+
body: { event_id: `evt_leave_${counter}` },
|
|
106
|
+
client,
|
|
107
|
+
say: async () => ({ ok: true, ts: "1.2" }),
|
|
108
|
+
});
|
|
109
|
+
return getSlackTasksInThread(channel, threadTs);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let lead: Agent;
|
|
113
|
+
let worker: Agent;
|
|
114
|
+
let otherWorker: Agent;
|
|
115
|
+
|
|
116
|
+
async function seedThread(channel: string, threadTs: string, opts: { unassigned?: boolean } = {}) {
|
|
117
|
+
// As the Slack handlers create it: only Slack ingress writes slackTriggerMessageTs.
|
|
118
|
+
return createTaskExtended("original ask", {
|
|
119
|
+
agentId: opts.unassigned ? undefined : worker.id,
|
|
120
|
+
source: "slack",
|
|
121
|
+
slackChannelId: channel,
|
|
122
|
+
slackThreadTs: threadTs,
|
|
123
|
+
slackTriggerMessageTs: threadTs,
|
|
124
|
+
slackUserId: HUMAN,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Counts how many times a workflow node actually runs. */
|
|
129
|
+
class RecordingExecutor extends BaseExecutor<
|
|
130
|
+
typeof RecordingExecutor.schema,
|
|
131
|
+
typeof RecordingExecutor.outSchema
|
|
132
|
+
> {
|
|
133
|
+
static readonly schema = z.object({ template: z.string().optional() });
|
|
134
|
+
static readonly outSchema = z.object({ sent: z.boolean() });
|
|
135
|
+
static runs = 0;
|
|
136
|
+
|
|
137
|
+
readonly type = "notify";
|
|
138
|
+
readonly mode = "instant" as const;
|
|
139
|
+
readonly configSchema = RecordingExecutor.schema;
|
|
140
|
+
readonly outputSchema = RecordingExecutor.outSchema;
|
|
141
|
+
|
|
142
|
+
protected async execute(): Promise<ExecutorResult<z.infer<typeof RecordingExecutor.outSchema>>> {
|
|
143
|
+
RecordingExecutor.runs++;
|
|
144
|
+
return { status: "success", output: { sent: true } };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let routeServer: Server;
|
|
149
|
+
let routeBaseUrl: string;
|
|
150
|
+
|
|
151
|
+
beforeAll(async () => {
|
|
152
|
+
initDb(TEST_DB_PATH);
|
|
153
|
+
process.env.SLACK_RENDER_V2 = "false";
|
|
154
|
+
process.env.ADDITIVE_SLACK = "false";
|
|
155
|
+
process.env.SLACK_BLOCK_EXTERNAL_CHANNELS = "false";
|
|
156
|
+
delete process.env.SLACK_LEAVE_THREAD_DISABLE;
|
|
157
|
+
lead = await createAgent({ name: "leave-lead", isLead: true, status: "idle", capabilities: [] });
|
|
158
|
+
worker = await createAgent({
|
|
159
|
+
name: "leave-worker",
|
|
160
|
+
isLead: false,
|
|
161
|
+
status: "idle",
|
|
162
|
+
capabilities: [],
|
|
163
|
+
});
|
|
164
|
+
otherWorker = await createAgent({
|
|
165
|
+
name: "leave-other",
|
|
166
|
+
isLead: false,
|
|
167
|
+
status: "idle",
|
|
168
|
+
capabilities: [],
|
|
169
|
+
});
|
|
170
|
+
registerMessageHandler({
|
|
171
|
+
event: (type: string, handler: Handler) => {
|
|
172
|
+
handlers.set(type, handler);
|
|
173
|
+
},
|
|
174
|
+
} as never);
|
|
175
|
+
registerCommandHandler({
|
|
176
|
+
command: (name: string, handler: Handler) => {
|
|
177
|
+
commands.set(name, handler);
|
|
178
|
+
},
|
|
179
|
+
} as never);
|
|
180
|
+
|
|
181
|
+
routeServer = createServer(async (req, res) => {
|
|
182
|
+
setRequestAuth(req, { kind: "operator", fingerprint: "test-key" });
|
|
183
|
+
const url = req.url ?? "/";
|
|
184
|
+
const handled = await handleTasks(
|
|
185
|
+
req,
|
|
186
|
+
res,
|
|
187
|
+
url.split("?")[0]?.split("/").filter(Boolean) ?? [],
|
|
188
|
+
new URLSearchParams(url.split("?")[1] ?? ""),
|
|
189
|
+
req.headers["x-agent-id"] as string | undefined,
|
|
190
|
+
);
|
|
191
|
+
if (!handled) res.writeHead(404).end();
|
|
192
|
+
});
|
|
193
|
+
routeBaseUrl = `http://127.0.0.1:${await listenOnFreePort(routeServer)}`;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
afterAll(async () => {
|
|
197
|
+
await new Promise<void>((resolve) => routeServer.close(() => resolve()));
|
|
198
|
+
closeDb();
|
|
199
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
200
|
+
try {
|
|
201
|
+
unlinkSync(`${TEST_DB_PATH}${suffix}`);
|
|
202
|
+
} catch {
|
|
203
|
+
// ignore
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const [key, value] of Object.entries(previousEnv)) {
|
|
207
|
+
if (value === undefined) delete process.env[key];
|
|
208
|
+
else process.env[key] = value;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
beforeEach(() => {
|
|
213
|
+
resetSlackHandlerCachesForTesting();
|
|
214
|
+
ephemerals.length = 0;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("slack-leave-thread", () => {
|
|
218
|
+
test("a left thread skips a plain reply; a mention clears the state and is processed", async () => {
|
|
219
|
+
const task = await seedThread("C_LEAVE_A", "1800.0001");
|
|
220
|
+
const result = await buildTool().handler({ taskId: task.id }, meta(worker.id));
|
|
221
|
+
expect(result.structuredContent.success).toBe(true);
|
|
222
|
+
expect(await isSlackThreadLeft("C_LEAVE_A", "1800.0001")).toBe(true);
|
|
223
|
+
|
|
224
|
+
expect(await sendThreadReply("C_LEAVE_A", "1800.0001", "thanks, we'll sort it")).toHaveLength(
|
|
225
|
+
1,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const afterMention = await sendThreadReply(
|
|
229
|
+
"C_LEAVE_A",
|
|
230
|
+
"1800.0001",
|
|
231
|
+
`<@${BOT_USER_ID}> one more thing`,
|
|
232
|
+
);
|
|
233
|
+
expect(afterMention).toHaveLength(2);
|
|
234
|
+
expect(await isSlackThreadLeft("C_LEAVE_A", "1800.0001")).toBe(false);
|
|
235
|
+
|
|
236
|
+
// Back in the thread: plain replies follow up again.
|
|
237
|
+
expect(await sendThreadReply("C_LEAVE_A", "1800.0001", "and also this")).toHaveLength(3);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("another thread is unaffected", async () => {
|
|
241
|
+
await seedThread("C_LEAVE_B", "1900.0001");
|
|
242
|
+
await buildTool().handler({ channelId: "C_LEAVE_B", threadTs: "1900.0002" }, meta(lead.id));
|
|
243
|
+
|
|
244
|
+
expect(await isSlackThreadLeft("C_LEAVE_B", "1900.0002")).toBe(true);
|
|
245
|
+
expect(await sendThreadReply("C_LEAVE_B", "1900.0001", "plain follow-up")).toHaveLength(2);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("refuses a task the agent has no context for, and missing arguments", async () => {
|
|
249
|
+
const task = await seedThread("C_LEAVE_C", "2000.0001");
|
|
250
|
+
const tool = buildTool();
|
|
251
|
+
|
|
252
|
+
const foreign = await tool.handler({ taskId: task.id }, meta(otherWorker.id));
|
|
253
|
+
expect(foreign.structuredContent.success).toBe(false);
|
|
254
|
+
expect(await isSlackThreadLeft("C_LEAVE_C", "2000.0001")).toBe(false);
|
|
255
|
+
|
|
256
|
+
const missing = await tool.handler({ channelId: "C_LEAVE_C" }, meta(worker.id));
|
|
257
|
+
expect(missing.structuredContent.success).toBe(false);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("only a lead can leave an arbitrary thread by channelId; KV callers cannot write the state", async () => {
|
|
261
|
+
const tool = buildTool();
|
|
262
|
+
const direct = await tool.handler(
|
|
263
|
+
{ channelId: "C_LEAVE_D", threadTs: "2100.0001" },
|
|
264
|
+
meta(worker.id),
|
|
265
|
+
);
|
|
266
|
+
expect(direct.structuredContent.success).toBe(false);
|
|
267
|
+
expect(await isSlackThreadLeft("C_LEAVE_D", "2100.0001")).toBe(false);
|
|
268
|
+
|
|
269
|
+
expect(reservedNamespaceError(SLACK_LEFT_THREAD_KV_NAMESPACE)).not.toBeNull();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("a non-lead cannot leave a thread through a task with hand-set Slack context", async () => {
|
|
273
|
+
const forged = await createTaskExtended("forged", {
|
|
274
|
+
agentId: worker.id,
|
|
275
|
+
creatorAgentId: worker.id,
|
|
276
|
+
source: "mcp",
|
|
277
|
+
slackChannelId: "C_LEAVE_E",
|
|
278
|
+
slackThreadTs: "2200.0001",
|
|
279
|
+
});
|
|
280
|
+
const tool = buildTool();
|
|
281
|
+
|
|
282
|
+
const denied = await tool.handler({ taskId: forged.id }, meta(worker.id));
|
|
283
|
+
expect(denied.structuredContent.success).toBe(false);
|
|
284
|
+
expect(await isSlackThreadLeft("C_LEAVE_E", "2200.0001")).toBe(false);
|
|
285
|
+
|
|
286
|
+
const byLead = await tool.handler({ taskId: forged.id }, meta(lead.id));
|
|
287
|
+
expect(byLead.structuredContent.success).toBe(true);
|
|
288
|
+
expect(await isSlackThreadLeft("C_LEAVE_E", "2200.0001")).toBe(true);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("a non-lead cannot leave a thread through a task it created over REST with source slack", async () => {
|
|
292
|
+
const channel = "C_FORGED_HTTP";
|
|
293
|
+
const threadTs = "1700000000.000001";
|
|
294
|
+
const res = await fetch(`${routeBaseUrl}/api/tasks`, {
|
|
295
|
+
method: "POST",
|
|
296
|
+
headers: { "content-type": "application/json", "x-agent-id": worker.id },
|
|
297
|
+
body: JSON.stringify({
|
|
298
|
+
task: "forged slack task",
|
|
299
|
+
agentId: worker.id,
|
|
300
|
+
routingReason: "skill",
|
|
301
|
+
source: "slack",
|
|
302
|
+
contextKey: slackContextKey({ channelId: channel, threadTs }),
|
|
303
|
+
}),
|
|
304
|
+
});
|
|
305
|
+
expect(res.status).toBe(201);
|
|
306
|
+
const forged = (await res.json()) as AgentTask;
|
|
307
|
+
expect(forged.source).toBe("slack");
|
|
308
|
+
expect(forged.slackChannelId).toBe(channel);
|
|
309
|
+
expect(forged.slackThreadTs).toBe(threadTs);
|
|
310
|
+
|
|
311
|
+
const denied = await buildTool().handler({ taskId: forged.id }, meta(worker.id));
|
|
312
|
+
expect(denied.structuredContent.success).toBe(false);
|
|
313
|
+
expect(await isSlackThreadLeft(channel, threadTs)).toBe(false);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("a plain reply in a left thread starts no workflow; a mention still does", async () => {
|
|
317
|
+
const workflow = await createWorkflow({
|
|
318
|
+
name: "leave-thread-slack-listener",
|
|
319
|
+
triggers: [{ type: "event", eventName: "slack.message" }],
|
|
320
|
+
definition: { nodes: [{ id: "n1", type: "notify", config: { template: "ping" } }] },
|
|
321
|
+
});
|
|
322
|
+
const registry = new ExecutorRegistry();
|
|
323
|
+
registry.register(new RecordingExecutor());
|
|
324
|
+
const emitted: unknown[] = [];
|
|
325
|
+
const started: Promise<string[]>[] = [];
|
|
326
|
+
const listener = (payload: unknown) => {
|
|
327
|
+
emitted.push(payload);
|
|
328
|
+
started.push(handleEventTrigger("slack.message", payload, registry));
|
|
329
|
+
};
|
|
330
|
+
workflowEventBus.on("slack.message", listener);
|
|
331
|
+
RecordingExecutor.runs = 0;
|
|
332
|
+
try {
|
|
333
|
+
const task = await seedThread("C_LEAVE_F", "2300.0001");
|
|
334
|
+
await buildTool().handler({ taskId: task.id }, meta(worker.id));
|
|
335
|
+
|
|
336
|
+
await sendThreadReply("C_LEAVE_F", "2300.0001", "people talking among themselves");
|
|
337
|
+
const mutedRuns = (await Promise.all(started)).flat();
|
|
338
|
+
expect(emitted).toHaveLength(0);
|
|
339
|
+
expect(mutedRuns).toHaveLength(0);
|
|
340
|
+
expect(RecordingExecutor.runs).toBe(0);
|
|
341
|
+
|
|
342
|
+
await sendThreadReply("C_LEAVE_F", "2300.0001", `<@${BOT_USER_ID}> back in`);
|
|
343
|
+
const mentionRuns = (await Promise.all(started)).flat();
|
|
344
|
+
expect(emitted).toHaveLength(1);
|
|
345
|
+
expect(mentionRuns).toHaveLength(1);
|
|
346
|
+
expect(RecordingExecutor.runs).toBe(1);
|
|
347
|
+
} finally {
|
|
348
|
+
workflowEventBus.off("slack.message", listener);
|
|
349
|
+
await updateWorkflow(workflow.id, { enabled: false });
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("replies buffered before the thread was left create no task when they flush", async () => {
|
|
354
|
+
const task = await seedThread("C_LEAVE_G", "2400.0001");
|
|
355
|
+
bufferThreadMessage("C_LEAVE_G", "2400.0001", "still chatting", HUMAN, "2400.0002");
|
|
356
|
+
await buildTool().handler({ taskId: task.id }, meta(worker.id));
|
|
357
|
+
|
|
358
|
+
await instantFlush("C_LEAVE_G:2400.0001");
|
|
359
|
+
expect(await getSlackTasksInThread("C_LEAVE_G", "2400.0001")).toHaveLength(1);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe("SLACK_LEAVE_THREAD_DISABLE", () => {
|
|
364
|
+
function registeredTools(server: McpServer): Record<string, unknown> {
|
|
365
|
+
return (server as unknown as { _registeredTools: Record<string, unknown> })._registeredTools;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
test("set at session creation, the tool is absent from the MCP surface", async () => {
|
|
369
|
+
expect(registeredTools(await createMcpServer())["slack-leave-thread"]).toBeDefined();
|
|
370
|
+
|
|
371
|
+
process.env.SLACK_LEAVE_THREAD_DISABLE = "true";
|
|
372
|
+
try {
|
|
373
|
+
const tools = registeredTools(await createMcpServer());
|
|
374
|
+
expect(tools["slack-leave-thread"]).toBeUndefined();
|
|
375
|
+
expect(tools["slack-reply"]).toBeDefined();
|
|
376
|
+
} finally {
|
|
377
|
+
delete process.env.SLACK_LEAVE_THREAD_DISABLE;
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// The scripts bridge caches the full-surface server it builds on first use.
|
|
382
|
+
test("the cached scripts bridge follows the flag per call, not the flag at first build", async () => {
|
|
383
|
+
function bridgeLeave(taskId: string) {
|
|
384
|
+
return invokeToolInProcess({
|
|
385
|
+
toolName: "slack-leave-thread",
|
|
386
|
+
args: { taskId },
|
|
387
|
+
agentId: worker.id,
|
|
388
|
+
callOrigin: "script-sdk",
|
|
389
|
+
}) as Promise<{ structuredContent: { success: boolean; message: string } }>;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
resetBridgeServerForTesting();
|
|
393
|
+
process.env.SLACK_LEAVE_THREAD_DISABLE = "true";
|
|
394
|
+
try {
|
|
395
|
+
const refused = await bridgeLeave((await seedThread("C_LEAVE_I", "2600.0001")).id);
|
|
396
|
+
expect(refused.structuredContent.success).toBe(false);
|
|
397
|
+
expect(refused.structuredContent.message).toContain("SLACK_LEAVE_THREAD_DISABLE");
|
|
398
|
+
|
|
399
|
+
delete process.env.SLACK_LEAVE_THREAD_DISABLE;
|
|
400
|
+
const left = await bridgeLeave((await seedThread("C_LEAVE_I", "2600.0002")).id);
|
|
401
|
+
expect(left.structuredContent.success).toBe(true);
|
|
402
|
+
expect(await isSlackThreadLeft("C_LEAVE_I", "2600.0002")).toBe(true);
|
|
403
|
+
} finally {
|
|
404
|
+
delete process.env.SLACK_LEAVE_THREAD_DISABLE;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
resetBridgeServerForTesting();
|
|
408
|
+
try {
|
|
409
|
+
expect(
|
|
410
|
+
(await bridgeLeave((await seedThread("C_LEAVE_I", "2600.0003")).id)).structuredContent
|
|
411
|
+
.success,
|
|
412
|
+
).toBe(true);
|
|
413
|
+
|
|
414
|
+
process.env.SLACK_LEAVE_THREAD_DISABLE = "true";
|
|
415
|
+
const refused = await bridgeLeave((await seedThread("C_LEAVE_I", "2600.0004")).id);
|
|
416
|
+
expect(refused.structuredContent.success).toBe(false);
|
|
417
|
+
expect(refused.structuredContent.message).toContain("SLACK_LEAVE_THREAD_DISABLE");
|
|
418
|
+
expect(await isSlackThreadLeft("C_LEAVE_I", "2600.0004")).toBe(false);
|
|
419
|
+
} finally {
|
|
420
|
+
delete process.env.SLACK_LEAVE_THREAD_DISABLE;
|
|
421
|
+
resetBridgeServerForTesting();
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("set through a global config reload after registration, the call fails naming the flag", async () => {
|
|
426
|
+
const task = await seedThread("C_LEAVE_H", "2500.0001");
|
|
427
|
+
const tool = buildTool();
|
|
428
|
+
const row = await upsertSwarmConfig({
|
|
429
|
+
scope: "global",
|
|
430
|
+
key: "SLACK_LEAVE_THREAD_DISABLE",
|
|
431
|
+
value: "true",
|
|
432
|
+
});
|
|
433
|
+
try {
|
|
434
|
+
await loadGlobalConfigsIntoEnv(true);
|
|
435
|
+
const refused = await tool.handler({ taskId: task.id }, meta(worker.id));
|
|
436
|
+
expect(refused.structuredContent.success).toBe(false);
|
|
437
|
+
expect(refused.structuredContent.message).toContain("SLACK_LEAVE_THREAD_DISABLE");
|
|
438
|
+
expect(await isSlackThreadLeft("C_LEAVE_H", "2500.0001")).toBe(false);
|
|
439
|
+
|
|
440
|
+
await deleteSwarmConfig(row.id);
|
|
441
|
+
await loadGlobalConfigsIntoEnv(true);
|
|
442
|
+
const left = await tool.handler({ taskId: task.id }, meta(worker.id));
|
|
443
|
+
expect(left.structuredContent.success).toBe(true);
|
|
444
|
+
expect(await isSlackThreadLeft("C_LEAVE_H", "2500.0001")).toBe(true);
|
|
445
|
+
} finally {
|
|
446
|
+
await deleteSwarmConfig(row.id);
|
|
447
|
+
__resetInjectedEnvTracking();
|
|
448
|
+
delete process.env.SLACK_LEAVE_THREAD_DISABLE;
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
describe("!leave", () => {
|
|
454
|
+
test("leaves the thread in code: no task, no workflow event, an ephemeral confirmation", async () => {
|
|
455
|
+
const emitted: unknown[] = [];
|
|
456
|
+
const listener = (payload: unknown) => emitted.push(payload);
|
|
457
|
+
workflowEventBus.on("slack.message", listener);
|
|
458
|
+
try {
|
|
459
|
+
await seedThread("C_BANG_A", "3000.0001");
|
|
460
|
+
expect(await sendThreadReply("C_BANG_A", "3000.0001", "!leave")).toHaveLength(1);
|
|
461
|
+
expect(emitted).toHaveLength(0);
|
|
462
|
+
} finally {
|
|
463
|
+
workflowEventBus.off("slack.message", listener);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
expect(await isSlackThreadLeft("C_BANG_A", "3000.0001")).toBe(true);
|
|
467
|
+
const entry = await getKv(SLACK_LEFT_THREAD_KV_NAMESPACE, "C_BANG_A:3000.0001");
|
|
468
|
+
expect(entry?.value).toMatchObject({ slackUserId: HUMAN });
|
|
469
|
+
expect(entry?.value).not.toHaveProperty("agentId");
|
|
470
|
+
expect(ephemerals).toEqual([
|
|
471
|
+
{
|
|
472
|
+
channel: "C_BANG_A",
|
|
473
|
+
user: HUMAN,
|
|
474
|
+
thread_ts: "3000.0001",
|
|
475
|
+
text: SLACK_LEFT_THREAD_CONFIRMATION,
|
|
476
|
+
},
|
|
477
|
+
]);
|
|
478
|
+
|
|
479
|
+
expect(await sendThreadReply("C_BANG_A", "3000.0001", "chatting among ourselves")).toHaveLength(
|
|
480
|
+
1,
|
|
481
|
+
);
|
|
482
|
+
expect(
|
|
483
|
+
await sendThreadReply("C_BANG_A", "3000.0001", `<@${BOT_USER_ID}> back please`),
|
|
484
|
+
).toHaveLength(2);
|
|
485
|
+
expect(await isSlackThreadLeft("C_BANG_A", "3000.0001")).toBe(false);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test("an @mention before !leave leaves instead of rejoining, also in a left thread", async () => {
|
|
489
|
+
await seedThread("C_BANG_B", "3100.0001");
|
|
490
|
+
await sendThreadReply("C_BANG_B", "3100.0001", "!leave");
|
|
491
|
+
expect(await sendThreadReply("C_BANG_B", "3100.0001", `<@${BOT_USER_ID}> !leave`)).toHaveLength(
|
|
492
|
+
1,
|
|
493
|
+
);
|
|
494
|
+
expect(await isSlackThreadLeft("C_BANG_B", "3100.0001")).toBe(true);
|
|
495
|
+
expect(ephemerals).toHaveLength(2);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("with ADDITIVE_SLACK the !leave message is never buffered", async () => {
|
|
499
|
+
process.env.ADDITIVE_SLACK = "true";
|
|
500
|
+
try {
|
|
501
|
+
await seedThread("C_BANG_C", "3200.0001");
|
|
502
|
+
await sendThreadReply("C_BANG_C", "3200.0001", "!leave");
|
|
503
|
+
expect(getBufferMessageCount("C_BANG_C:3200.0001")).toBe(0);
|
|
504
|
+
expect(await isSlackThreadLeft("C_BANG_C", "3200.0001")).toBe(true);
|
|
505
|
+
} finally {
|
|
506
|
+
process.env.ADDITIVE_SLACK = "false";
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
test.each([
|
|
511
|
+
["!leave", "3600.0001"],
|
|
512
|
+
[`<@${BOT_USER_ID}> !leave`, "3600.0002"],
|
|
513
|
+
])("leaves a human-rooted thread whose only task is unassigned: %s", async (text, threadTs) => {
|
|
514
|
+
const emitted: unknown[] = [];
|
|
515
|
+
const listener = (payload: unknown) => emitted.push(payload);
|
|
516
|
+
workflowEventBus.on("slack.message", listener);
|
|
517
|
+
try {
|
|
518
|
+
const seeded = await seedThread("C_BANG_G", threadTs, { unassigned: true });
|
|
519
|
+
expect(seeded.agentId).toBeNull();
|
|
520
|
+
expect(await sendThreadReply("C_BANG_G", threadTs, text)).toHaveLength(1);
|
|
521
|
+
expect(emitted).toHaveLength(0);
|
|
522
|
+
} finally {
|
|
523
|
+
workflowEventBus.off("slack.message", listener);
|
|
524
|
+
}
|
|
525
|
+
expect(await isSlackThreadLeft("C_BANG_G", threadTs)).toBe(true);
|
|
526
|
+
expect(ephemerals).toHaveLength(1);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test("a task in another thread of the channel does not make !leave a command", async () => {
|
|
530
|
+
await seedThread("C_BANG_H", "3700.0001", { unassigned: true });
|
|
531
|
+
await sendThreadReply("C_BANG_H", "3700.0002", "!leave");
|
|
532
|
+
expect(await isSlackThreadLeft("C_BANG_H", "3700.0002")).toBe(false);
|
|
533
|
+
expect(ephemerals).toHaveLength(0);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test("is not a command outside a swarm thread, when addressed elsewhere, or with extra text", async () => {
|
|
537
|
+
await sendThreadReply("C_BANG_D", "3300.0001", "!leave");
|
|
538
|
+
expect(await isSlackThreadLeft("C_BANG_D", "3300.0001")).toBe(false);
|
|
539
|
+
|
|
540
|
+
await seedThread("C_BANG_E", "3400.0001");
|
|
541
|
+
await sendThreadReply("C_BANG_E", "3400.0001", "<@U_SOMEONE_ELSE> !leave");
|
|
542
|
+
await sendThreadReply("C_BANG_E", "3400.0001", "!leave it to me");
|
|
543
|
+
expect(await isSlackThreadLeft("C_BANG_E", "3400.0001")).toBe(false);
|
|
544
|
+
expect(ephemerals).toHaveLength(0);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("SLACK_LEAVE_THREAD_DISABLE makes !leave an ordinary message and hides it from help", async () => {
|
|
548
|
+
async function helpText(): Promise<string> {
|
|
549
|
+
let text = "";
|
|
550
|
+
await commands.get("/agent-swarm-help")!({
|
|
551
|
+
ack: async () => {},
|
|
552
|
+
respond: async (reply: { blocks: Array<{ text: { text: string } }> }) => {
|
|
553
|
+
text = reply.blocks.map((b) => b.text.text).join("\n");
|
|
554
|
+
},
|
|
555
|
+
});
|
|
556
|
+
return text;
|
|
557
|
+
}
|
|
558
|
+
expect(await helpText()).toContain("`!leave` in a thread");
|
|
559
|
+
|
|
560
|
+
process.env.SLACK_LEAVE_THREAD_DISABLE = "true";
|
|
561
|
+
try {
|
|
562
|
+
await seedThread("C_BANG_F", "3500.0001");
|
|
563
|
+
expect(await sendThreadReply("C_BANG_F", "3500.0001", "!leave")).toHaveLength(2);
|
|
564
|
+
expect(await isSlackThreadLeft("C_BANG_F", "3500.0001")).toBe(false);
|
|
565
|
+
expect(ephemerals).toHaveLength(0);
|
|
566
|
+
expect(await helpText()).not.toContain("!leave");
|
|
567
|
+
} finally {
|
|
568
|
+
delete process.env.SLACK_LEAVE_THREAD_DISABLE;
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
1
2
|
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
2
3
|
import {
|
|
3
4
|
closeDb,
|
|
@@ -6,6 +7,7 @@ import {
|
|
|
6
7
|
getDb,
|
|
7
8
|
getDbClient,
|
|
8
9
|
getResolvableDeferralOutcomes,
|
|
10
|
+
getSlackTreeMessages,
|
|
9
11
|
initDb,
|
|
10
12
|
recordSlackMessage,
|
|
11
13
|
} from "../be/db";
|
|
@@ -93,12 +95,32 @@ describe("slack render-v2 watcher queries", () => {
|
|
|
93
95
|
closeDb();
|
|
94
96
|
});
|
|
95
97
|
|
|
98
|
+
test("the tree sweep tests thread tasks in the covering index, not task rows", async () => {
|
|
99
|
+
const sql = await capturedSql(() => getSlackTreeMessages());
|
|
100
|
+
const detail = plan(sql);
|
|
101
|
+
const uses = "SEARCH task USING COVERING INDEX idx_agent_tasks_slack_thread_cover";
|
|
102
|
+
expect(detail.split(uses).length - 1).toBe(3);
|
|
103
|
+
// Control: without migration 264 the same tests read the task rows.
|
|
104
|
+
const copy = Database.deserialize(getDb().serialize());
|
|
105
|
+
try {
|
|
106
|
+
copy.run("DROP INDEX idx_agent_tasks_slack_thread_cover");
|
|
107
|
+
const reverted = copy
|
|
108
|
+
.query<{ detail: string }, []>(`EXPLAIN QUERY PLAN ${sql}`)
|
|
109
|
+
.all()
|
|
110
|
+
.map((row) => row.detail)
|
|
111
|
+
.join("\n");
|
|
112
|
+
expect(reverted).not.toContain(uses);
|
|
113
|
+
} finally {
|
|
114
|
+
copy.close();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
96
118
|
test("deferral resolution drives from the chain, not from every task", async () => {
|
|
97
119
|
const sql = await capturedSql(() => getResolvableDeferralOutcomes());
|
|
98
120
|
const detail = plan(sql, [50]);
|
|
99
|
-
//
|
|
121
|
+
// Any parentTaskId-led index is a seek on the chain's children.
|
|
100
122
|
expect(detail).toMatch(
|
|
101
|
-
/SEARCH wake USING INDEX idx_agent_tasks_parentTaskId(_source)? \(parentTaskId=\?\)/,
|
|
123
|
+
/SEARCH wake USING INDEX idx_agent_tasks_parentTaskId(_source|_id)? \(parentTaskId=\?\)/,
|
|
102
124
|
);
|
|
103
125
|
expect(detail).not.toContain("idx_agent_tasks_taskType");
|
|
104
126
|
expect(detail).not.toContain("idx_agent_tasks_status_last_updated");
|
package/src/tests/status.test.ts
CHANGED
|
@@ -87,6 +87,8 @@ const ENV_KEYS_TO_RESET = [
|
|
|
87
87
|
"SWARM_BRAND_COLOR",
|
|
88
88
|
"SWARM_MARKETING_URL",
|
|
89
89
|
"SWARM_HIDE_CLOUD_PROMO",
|
|
90
|
+
"APP_URL",
|
|
91
|
+
"DASHBOARD_URL",
|
|
90
92
|
"HARNESS_PROVIDER",
|
|
91
93
|
"ANTHROPIC_API_KEY",
|
|
92
94
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
@@ -191,6 +193,7 @@ describe("buildStatusPayload — identity", () => {
|
|
|
191
193
|
marketing_url: null,
|
|
192
194
|
hide_cloud_promo: false,
|
|
193
195
|
org_id: null,
|
|
196
|
+
dashboard_url: null,
|
|
194
197
|
});
|
|
195
198
|
});
|
|
196
199
|
|
|
@@ -212,9 +215,17 @@ describe("buildStatusPayload — identity", () => {
|
|
|
212
215
|
marketing_url: "https://swarm.acme.example",
|
|
213
216
|
hide_cloud_promo: true,
|
|
214
217
|
org_id: "org_acme_123",
|
|
218
|
+
dashboard_url: null,
|
|
215
219
|
});
|
|
216
220
|
});
|
|
217
221
|
|
|
222
|
+
test("reports the configured dashboard URL: the first APP_URL entry, else DASHBOARD_URL", async () => {
|
|
223
|
+
process.env.DASHBOARD_URL = "https://old.example.com";
|
|
224
|
+
expect((await buildStatusPayload()).identity.dashboard_url).toBe("https://old.example.com");
|
|
225
|
+
process.env.APP_URL = "https://swarm.example.com/, https://other.example.com";
|
|
226
|
+
expect((await buildStatusPayload()).identity.dashboard_url).toBe("https://swarm.example.com");
|
|
227
|
+
});
|
|
228
|
+
|
|
218
229
|
test("treats SWARM_CLOUD=1 the same as 'true'", async () => {
|
|
219
230
|
process.env.SWARM_CLOUD = "1";
|
|
220
231
|
const payload = await buildStatusPayload();
|