@desplega.ai/agent-swarm 2.4.4 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/{acp-adapter-thq04adh.js → acp-adapter-11gp67nr.js} +8 -6
- package/dist/{actions-j6ywh6x5.js → actions-8p2kze8j.js} +24 -22
- package/dist/{amp-adapter-k8b5xmj8.js → amp-adapter-z4qvhacj.js} +2 -2
- package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
- package/dist/{app-rb7vc02s.js → app-0qp0a2gk.js} +3 -3
- package/dist/{assistant-ftta1zjf.js → assistant-jedd3ktm.js} +27 -25
- package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
- package/dist/{boot-reembed-xns7h5h9.js → boot-reembed-4xwq5tg0.js} +1 -1
- package/dist/{boot-reembed-srp6v6af.js → boot-reembed-n4wq5dd2.js} +1 -1
- package/dist/{boot-scrub-sweep-3qqrh2mz.js → boot-scrub-sweep-69h90xcq.js} +4 -4
- package/dist/{boot-seeds-9ks8hce7.js → boot-seeds-3vn0m9sg.js} +4 -4
- package/dist/{claude-adapter-k5ksyres.js → claude-adapter-221mvad5.js} +8 -6
- package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-gt6pa35d.js} +2 -2
- package/dist/{claude-managed-setup-n5a5eqks.js → claude-managed-setup-c3hp36ph.js} +4 -4
- package/dist/{claude-sdk-session-2j3j4kwn.js → claude-sdk-session-8thjsk5e.js} +9729 -9628
- package/dist/{cli-b9fcxxev.js → cli-007cnhm1.js} +50 -4
- package/dist/{cli-j4rqdf2n.js → cli-03vpq45j.js} +2 -2
- package/dist/{cli-amyhm3gj.js → cli-0y6f0ckq.js} +1 -1
- package/dist/{cli-4dg55wsk.js → cli-0z3vjrpm.js} +1 -1
- package/dist/{cli-pccfmtb5.js → cli-179xcyky.js} +11 -11
- package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
- package/dist/{cli-hj3debgw.js → cli-1pr672hz.js} +3 -3
- package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
- package/dist/{cli-t7cyef5g.js → cli-36j4jnk3.js} +4 -4
- package/dist/{cli-er83cjbn.js → cli-3qv8naq8.js} +527 -231
- package/dist/cli-3zwye9sn.js +1193 -0
- package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
- package/dist/{cli-xj0re5tm.js → cli-4w24f7z3.js} +1 -1
- package/dist/{cli-v3yr2gbg.js → cli-5kzk3472.js} +2198 -968
- package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
- package/dist/{cli-w6nadkh4.js → cli-5rvg1ddk.js} +5 -5
- package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
- package/dist/{cli-9qtjfd60.js → cli-6rg2jn2y.js} +3 -2
- package/dist/{cli-2v045xyf.js → cli-7bwtc6yx.js} +3 -3
- package/dist/{cli-3cgngyd8.js → cli-7g40g8hq.js} +35 -17
- package/dist/{cli-q6r7t299.js → cli-92mqenkc.js} +2379 -154
- package/dist/{cli-cynvc9fj.js → cli-9ce2xw98.js} +8 -8
- package/dist/{cli-gqvh8d8a.js → cli-bgdtv62r.js} +36 -22
- package/dist/{cli-wxbn3yfx.js → cli-cz5nmdxt.js} +2 -2
- package/dist/{cli-wt645v7q.js → cli-dabehxhp.js} +1 -1
- package/dist/{cli-vfha21c9.js → cli-e5bxnjps.js} +2 -2
- package/dist/{cli-p20r8w42.js → cli-fcc5qe3f.js} +763 -268
- package/dist/{cli-1ephgwnq.js → cli-fes51ya2.js} +3 -3
- package/dist/{cli-1d70crsy.js → cli-ftmq4nvk.js} +2 -2
- package/dist/{cli-qkhc5aqy.js → cli-fy1jyyym.js} +13 -13
- package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
- package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
- package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
- package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
- package/dist/{cli-bnz0r3kc.js → cli-j1eghqfm.js} +5 -5
- package/dist/{cli-2zqpkvtt.js → cli-jpxsrxcb.js} +1 -1
- package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
- package/dist/{cli-37yb7307.js → cli-mbfj11fq.js} +1 -1
- package/dist/{cli-pnp2j5zg.js → cli-mmhj8vd7.js} +2 -2
- package/dist/{cli-xf234ws8.js → cli-n4c8e3s5.js} +2 -0
- package/dist/{cli-a0kcty51.js → cli-n6kmqngw.js} +2 -2
- package/dist/{cli-te6b5wvg.js → cli-nnvjb17s.js} +3 -3
- package/dist/{cli-1dmcq2eh.js → cli-nzfnt2gs.js} +1 -1
- package/dist/{cli-625tb7aq.js → cli-p859hcc8.js} +2 -2
- package/dist/{cli-smvcw9z5.js → cli-pasyvjmm.js} +1 -1
- package/dist/cli-pkaqk57w.js +128 -0
- package/dist/{cli-8nyhf7za.js → cli-qqzmjxkf.js} +129 -105
- package/dist/{cli-zrwpctp4.js → cli-r25qpaxt.js} +161 -63
- package/dist/{cli-nc35pxb7.js → cli-rh5smt8t.js} +19 -4
- package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
- package/dist/{cli-esxerzg7.js → cli-s4sntwt4.js} +2 -2
- package/dist/{cli-b8k7epj1.js → cli-segs7hx9.js} +8 -8
- package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
- package/dist/{cli-vdm20wej.js → cli-sy8f9705.js} +4 -4
- package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
- package/dist/{cli-2y91wnbh.js → cli-we0ra057.js} +222 -10
- package/dist/{cli-55qmbp6z.js → cli-x21dqn18.js} +5 -5
- package/dist/{cli-dqb4n629.js → cli-x6vf4sad.js} +11 -11
- package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
- package/dist/{cli-vq97g1jq.js → cli-xawhpgc9.js} +95 -195
- package/dist/{cli-82d3nkc3.js → cli-xd4eqtr2.js} +2 -2
- package/dist/{cli-9fhkkpj5.js → cli-xt768r5m.js} +2 -2
- package/dist/{cli-zrk6jxv8.js → cli-y4x8ww6s.js} +2 -2
- package/dist/{cli-xc27fwzg.js → cli-y9jvxfhk.js} +3 -3
- package/dist/{cli-44159k28.js → cli-z63a269p.js} +2 -2
- package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
- package/dist/cli.js +31 -29
- package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
- package/dist/{codex-adapter-968xwfy7.js → codex-adapter-tcg1q3jk.js} +6 -4
- package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
- package/dist/{codex-session-runner-ht4z99fa.js → codex-session-runner-xfx4wd9x.js} +7 -5
- package/dist/{commands-pcymaeg9.js → commands-ew83nj70.js} +2 -2
- package/dist/{controller-fpjyk46r.js → controller-gnq1k1mf.js} +4 -4
- package/dist/{controllers-connect-mwzx1fh6.js → controllers-connect-tw1dan9r.js} +26 -24
- package/dist/{cursor-adapter-cp4s2e2f.js → cursor-adapter-yq5xgfcg.js} +1 -1
- package/dist/{db-xsjg9jqq.js → db-7gbc53fd.js} +14 -2
- package/dist/{db-sgd12bt9.js → db-t86xrnes.js} +3 -3
- package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
- package/dist/{dispatcher-x0jfw2c9.js → dispatcher-64bdyhps.js} +23 -21
- package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
- package/dist/{dsh-adapter-jrq7ma7h.js → dsh-adapter-tvhc4k2d.js} +1 -1
- package/dist/{e2b-16d6ch39.js → e2b-9gkdhm3c.js} +2 -2
- package/dist/{e2b-zn0b4w5w.js → e2b-cjtf8k1z.js} +2 -2
- package/dist/{easter-egg-3d-5cgynaja.js → easter-egg-3d-e4qv8dxj.js} +1 -1
- package/dist/{event-bus-9vpk8gmq.js → event-bus-3z78ep5n.js} +1 -1
- package/dist/{execute-t4e6m3rw.js → execute-0xpz8950.js} +85 -23
- package/dist/{external-channel-8njj4h3b.js → external-channel-e9nw24zv.js} +2 -2
- package/dist/{google-generative-ai-bmq8gkf3.js → google-generative-ai-mk9vcpe1.js} +15 -15
- package/dist/{google-vertex-b1rvp52s.js → google-vertex-q580kz8k.js} +20 -20
- package/dist/{grok-adapter-kkr97v2f.js → grok-adapter-v82srhrx.js} +9 -7
- package/dist/handlers-c9ajzhbn.js +41 -0
- package/dist/{heartbeat-wd03zhn0.js → heartbeat-2vqs1319.js} +26 -24
- package/dist/{hook-xzj2ts2p.js → hook-01nk13wx.js} +7 -5
- package/dist/{hook-f0vghj42.js → hook-jzpheegg.js} +9 -7
- package/dist/{http-4hwxv62p.js → http-fc1k6ec8.js} +859 -252
- package/dist/index-07ef7nkm.js +42 -0
- package/dist/{index-g6tbb6ve.js → index-31ceyf42.js} +1 -1
- package/dist/index-7yts8n9s.js +43 -0
- package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
- package/dist/index-q3t40ryv.js +46 -0
- package/dist/{index-ww2vznkp.js → index-sepja052.js} +1 -1
- package/dist/{index-97q4xn03.js → index-zdycv3ja.js} +135 -79
- package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
- package/dist/{keepalive-hrh7xnr9.js → keepalive-5nh2rhmr.js} +4 -4
- package/dist/lead-aayq64jn.js +79 -0
- package/dist/{llama-cpp-classify-vv43gswy.js → llama-cpp-classify-ee7vmd7q.js} +4 -1
- package/dist/{maintenance-e8wnwnqx.js → maintenance-hjhzdt3k.js} +1 -1
- package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
- package/dist/{node-3v3f08f7.js → node-jzjj7r9x.js} +10 -9
- package/dist/{oauth-refresh-sweep-zm0yj4sj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
- package/dist/{onboard-dtwcq603.js → onboard-1zq3m7db.js} +2 -2
- package/dist/{openai-codex-responses-de9pvs4d.js → openai-codex-responses-79s8xnbh.js} +29 -23
- package/dist/{openai-completions-mtqge3nf.js → openai-completions-tdbyts0x.js} +21 -20
- package/dist/{openai-decisions-ygk4kkxd.js → openai-decisions-bqhnjyfz.js} +2 -1
- package/dist/{openai-responses-dkyng71g.js → openai-responses-dm3t5vqp.js} +26 -17
- package/dist/{opencode-adapter-x4tqzhkh.js → opencode-adapter-e56086m1.js} +7 -5
- package/dist/{openrouter-images-wvv7camj.js → openrouter-images-ssttr1y4.js} +6 -3
- package/dist/{otel-impl-w3fbhpn0.js → otel-impl-h9yyza47.js} +2 -2
- package/dist/{pi-messages-nhh1pkz2.js → pi-messages-3nf8bq1a.js} +6 -4
- package/dist/{pi-mono-adapter-mcfkn9vf.js → pi-mono-adapter-tzps8f38.js} +9 -37
- package/dist/{pricing-refresh-j38dehfs.js → pricing-refresh-nevhat8h.js} +3 -3
- package/dist/{rbac-roles-ae8yttq4.js → rbac-roles-4y6ngvzq.js} +1 -1
- package/dist/{rbac-roles-ejxx9b35.js → rbac-roles-exbg8xb6.js} +2 -2
- package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
- package/dist/{render-v2-n16n0phe.js → render-v2-cfsh13z3.js} +3 -3
- package/dist/{resume-z0fgjc38.js → resume-5s2t0dbs.js} +10 -8
- package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
- package/dist/{session-access-cv3mq6mc.js → session-access-dnbyp3cn.js} +6 -4
- package/dist/{setup-mafhj3yt.js → setup-1wj1y77j.js} +1 -1
- package/dist/{typesafe-system-one-2hgpayzy.js → typesafe-system-one-bre38hvs.js} +4 -2
- package/dist/{virtual-modules-4r3zkc20.js → virtual-modules-mnfbh5mr.js} +8 -2
- package/dist/{work-objects-8b86mgjp.js → work-objects-7mg6216g.js} +26 -24
- package/dist/worker-nwb789mg.js +79 -0
- package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
- package/openapi.json +793 -6
- package/package.json +11 -11
- package/src/apps/catalog.generated.json +206 -4
- package/src/apps/page-validator.ts +13 -1
- package/src/assets/key.ts +17 -0
- package/src/azure-devops/handlers.ts +1 -1
- package/src/be/chunking.ts +42 -9
- package/src/be/db/approvals.ts +18 -0
- package/src/be/db/session-moves.ts +164 -0
- package/src/be/db/session-tree.ts +384 -0
- package/src/be/db/tasks/draft-text.ts +15 -0
- package/src/be/db/tasks/read.ts +54 -5
- package/src/be/db/workflow-runs.ts +96 -0
- package/src/be/db.ts +85 -237
- package/src/be/memory/index-content.ts +36 -17
- package/src/be/memory/providers/openai-embedding.ts +115 -61
- package/src/be/memory/providers/sqlite-store.ts +69 -23
- package/src/be/memory/reranker.ts +31 -1
- package/src/be/memory/search-candidates.ts +47 -0
- package/src/be/memory/types.ts +14 -0
- package/src/be/onboarding.ts +3 -0
- package/src/be/scripts/typecheck.ts +2 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +106 -54
- package/src/be/seed-scripts/index.ts +2 -2
- package/src/be/skill-sync.ts +43 -2
- package/src/be/swarm-config-guard.ts +25 -1
- package/src/be/task-attachment-store.ts +30 -0
- package/src/bitbucket/api.ts +81 -0
- package/src/bitbucket/auth.ts +64 -0
- package/src/bitbucket/handlers.ts +434 -0
- package/src/bitbucket/index.ts +28 -0
- package/src/bitbucket/templates.ts +134 -0
- package/src/bitbucket/types.ts +65 -0
- package/src/commands/runner.ts +55 -3
- package/src/e2b/env.ts +1 -0
- package/src/extensions/catalog.generated.json +4 -4
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +9 -2
- package/src/extensions/post-bridge.ts +1 -0
- package/src/fs/provider.ts +13 -1
- package/src/github/handlers.ts +6 -6
- package/src/gitlab/handlers.ts +5 -5
- package/src/heartbeat/heartbeat.ts +31 -11
- package/src/http/assets.ts +14 -2
- package/src/http/core.ts +12 -0
- package/src/http/fs.ts +54 -34
- package/src/http/index.ts +4 -0
- package/src/http/mcp-body.ts +37 -0
- package/src/http/mcp-user.ts +3 -5
- package/src/http/mcp.ts +6 -7
- package/src/http/memory.ts +26 -15
- package/src/http/onboarding.ts +1 -0
- package/src/http/page-proxy.ts +61 -5
- package/src/http/pages-public.ts +27 -44
- package/src/http/pages.ts +4 -4
- package/src/http/script-runs.ts +2 -0
- package/src/http/sessions.ts +366 -12
- package/src/http/skills.ts +48 -1
- package/src/http/tasks.ts +1 -1
- package/src/http/users.ts +13 -1
- package/src/http/webhooks.ts +141 -0
- package/src/integrations/chat-sdk/adapters.ts +24 -4
- package/src/integrations/chat-sdk/attachments.ts +90 -12
- package/src/integrations/chat-sdk/facts.ts +138 -0
- package/src/integrations/chat-sdk/inbound.ts +156 -8
- package/src/integrations/chat-sdk/limits.ts +24 -0
- package/src/integrations/chat-sdk/outbound-files.ts +186 -0
- package/src/integrations/chat-sdk/outbound.ts +272 -5
- package/src/integrations/chat-sdk/relay.ts +4 -1
- package/src/integrations/chat-sdk/router.ts +28 -2
- package/src/integrations/chat-sdk/steering.ts +9 -0
- package/src/integrations/chat-sdk/templates.ts +14 -0
- package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
- package/src/mcp-limits.ts +20 -0
- package/src/pages/browser-sdk.ts +8 -3
- package/src/pages/feedback-overlay.ts +1 -1
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +106 -119
- package/src/providers/codex-agents-md.ts +35 -229
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +5 -53
- package/src/realtime/transport.ts +1 -2
- package/src/script-workflows/workflow-ctx.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +2 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +14 -4
- package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
- package/src/server-user.ts +2 -0
- package/src/server.ts +2 -0
- package/src/slack/assistant.ts +2 -2
- package/src/slack/files.ts +2 -0
- package/src/slack/handlers.test.ts +158 -0
- package/src/slack/handlers.ts +23 -32
- package/src/slack/inbound-files.ts +39 -6
- package/src/slack/thread-buffer.ts +2 -1
- package/src/tasks/context-key.ts +61 -1
- package/src/tasks/task-terminal-effects.ts +21 -17
- package/src/tests/apps-card-grid.test.ts +197 -0
- package/src/tests/apps-catalog-additions.test.ts +120 -0
- package/src/tests/apps-tree.test.ts +129 -0
- package/src/tests/bitbucket-auth.test.ts +156 -0
- package/src/tests/bitbucket-handlers.test.ts +425 -0
- package/src/tests/bitbucket-webhook.test.ts +220 -0
- package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
- package/src/tests/chat-sdk-config.test.ts +11 -0
- package/src/tests/chat-sdk-facts.test.ts +23 -0
- package/src/tests/chat-sdk-inbound.test.ts +597 -4
- package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
- package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
- package/src/tests/codex-adapter.test.ts +167 -242
- package/src/tests/config-api-only-keys.test.ts +9 -0
- package/src/tests/context-key.test.ts +50 -0
- package/src/tests/core-auth.test.ts +5 -0
- package/src/tests/cors-allowlist.test.ts +1 -1
- package/src/tests/db-task-read-contract.test.ts +7 -0
- package/src/tests/db-task-write-contract.test.ts +6 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
- package/src/tests/fixtures/bitbucket/pullrequest-comment-created-inline.json +174 -0
- package/src/tests/fixtures/bitbucket/pullrequest-comment-created.json +170 -0
- package/src/tests/fixtures/bitbucket/pullrequest-created-mention.json +132 -0
- package/src/tests/fixtures/bitbucket/pullrequest-created-reviewer.json +140 -0
- package/src/tests/fixtures/bitbucket/pullrequest-fulfilled.json +134 -0
- package/src/tests/fixtures/bitbucket/pullrequest-rejected.json +133 -0
- package/src/tests/generate-mcp-docs.test.ts +101 -0
- package/src/tests/gitlab-handlers.test.ts +3 -3
- package/src/tests/gitlab-vcs-db.test.ts +4 -4
- package/src/tests/heartbeat-checklist.test.ts +33 -0
- package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
- package/src/tests/heartbeat-jev-gate.test.ts +242 -1
- package/src/tests/integration-disable-flags.test.ts +36 -0
- package/src/tests/kv-page-proxy.test.ts +91 -5
- package/src/tests/launch-password-rejection.test.ts +2 -2
- package/src/tests/mcp-input-caps.test.ts +294 -0
- package/src/tests/mcp-tools.test.ts +1 -1
- package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
- package/src/tests/memory-embedding-input-cap.test.ts +114 -0
- package/src/tests/memory.test.ts +14 -0
- package/src/tests/onboarding.test.ts +12 -0
- package/src/tests/page-guest-session.test.ts +19 -8
- package/src/tests/page-proxy-authed.test.ts +1 -1
- package/src/tests/page-proxy.test.ts +17 -10
- package/src/tests/page-session.test.ts +102 -1
- package/src/tests/pages-authed-mode.test.ts +82 -12
- package/src/tests/pages-password-mode.test.ts +5 -5
- package/src/tests/pi-codemode.test.ts +8 -2
- package/src/tests/preload-env.test.ts +57 -0
- package/src/tests/preload.ts +21 -0
- package/src/tests/prompt-template-remaining.test.ts +38 -0
- package/src/tests/realtime-transport.test.ts +13 -7
- package/src/tests/run-root-coverage.test.ts +39 -0
- package/src/tests/runner-bitbucket-vcs.test.ts +191 -0
- package/src/tests/runner-skills-refresh.test.ts +58 -0
- package/src/tests/script-runs-http.test.ts +84 -0
- package/src/tests/scripts-http.test.ts +10 -1
- package/src/tests/secret-scrubber.test.ts +15 -0
- package/src/tests/sessions-move.test.ts +576 -0
- package/src/tests/sessions-tree.test.ts +493 -0
- package/src/tests/skill-files-http.test.ts +50 -0
- package/src/tests/skills-bundle.test.ts +99 -0
- package/src/tests/swarm-config-export-to-env.test.ts +23 -3
- package/src/tests/task-audio-transcription.test.ts +299 -0
- package/src/tests/task-completion-memory-chunking.test.ts +434 -0
- package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
- package/src/tests/transcription.test.ts +299 -0
- package/src/tests/vcs-bitbucket.test.ts +91 -0
- package/src/tests/vcs-provider-isolation.test.ts +123 -0
- package/src/tests/vcs-provider.test.ts +16 -1
- package/src/tests/vcs-tracking.test.ts +3 -3
- package/src/tests/workflow-foreach-bodies.test.ts +637 -0
- package/src/tests/workflow-foreach-concurrency.test.ts +342 -0
- package/src/tests/workflow-foreach-retry.test.ts +537 -0
- package/src/tests/workflow-foreach.test.ts +44 -23
- package/src/tests/workflow-step-claims.test.ts +6 -6
- package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
- package/src/tests/workflow-system-one-decision.test.ts +2 -1
- package/src/tests/workflow-tla-races.test.ts +2 -2
- package/src/tools/chat-access.ts +10 -0
- package/src/tools/chat-post.ts +31 -11
- package/src/tools/chat-reply.ts +32 -10
- package/src/tools/memory-search.ts +3 -1
- package/src/tools/script-delete.ts +1 -1
- package/src/tools/system-one-decide.ts +17 -4
- package/src/tools/workflows/create-workflow.ts +5 -3
- package/src/types.ts +9 -2
- package/src/utils/config-env-export.ts +4 -1
- package/src/utils/page-session.ts +133 -20
- package/src/utils/secret-scrubber.ts +4 -0
- package/src/utils/skills-refresh.ts +83 -43
- package/src/utils/transcription.ts +246 -0
- package/src/vcs/bitbucket.ts +58 -0
- package/src/vcs/index.ts +2 -0
- package/src/vcs/types.ts +2 -2
- package/src/workflows/approval-resolution.ts +4 -1
- package/src/workflows/checkpoint.ts +9 -1
- package/src/workflows/constants.ts +3 -0
- package/src/workflows/definition.ts +48 -17
- package/src/workflows/engine.ts +25 -15
- package/src/workflows/executors/foreach.ts +45 -99
- package/src/workflows/executors/system-one-decision.ts +28 -7
- package/src/workflows/executors/system-one-providers.ts +39 -4
- package/src/workflows/foreach-body.ts +146 -0
- package/src/workflows/foreach-driver.ts +552 -0
- package/src/workflows/foreach-join.ts +206 -7
- package/src/workflows/node-timeout.ts +21 -0
- package/src/workflows/recovery.ts +68 -3
- package/src/workflows/resume.ts +221 -43
- package/src/workflows/retry-poller.ts +39 -24
- package/src/workflows/step-input.ts +59 -0
- package/src/workflows/task-step-routing.ts +114 -56
- package/templates/extensions/slack-reply-gate/README.md +24 -22
- package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
- package/templates/extensions/slack-reply-gate/manifest.json +2 -2
- package/templates/skills/apps/SKILL.md +28 -6
- package/templates/skills/apps/content.md +28 -6
- package/templates/skills/chat-sdk/content.md +6 -2
- package/templates/skills/code-quality/content.md +2 -1
- package/templates/skills/heartbeat-runbook/content.md +5 -5
- package/templates/skills/pages/content.md +6 -4
- package/templates/skills/slack-interaction/content.md +5 -0
- package/templates/skills/swarm-scripts/SKILL.md +2 -2
- package/templates/skills/swarm-scripts/content.md +2 -2
- package/templates/skills/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
- package/dist/cli-51y09c6p.js +0 -2657
- package/dist/cli-yw91rnmw.js +0 -13
- package/dist/handlers-0pvee9nw.js +0 -39
- package/dist/index-gdh8jrdm.js +0 -43
- package/dist/index-hg0x2cdv.js +0 -40
- package/dist/index-thm0y6a8.js +0 -41
- package/dist/lead-ee8vazz2.js +0 -77
- package/dist/worker-16s75hm5.js +0 -77
|
@@ -1,23 +1,33 @@
|
|
|
1
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
2
2
|
import { mkdtemp, rm, unlink } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
6
|
import {
|
|
6
7
|
closeDb,
|
|
8
|
+
completeTask,
|
|
7
9
|
createAgent,
|
|
10
|
+
createTaskExtended,
|
|
11
|
+
createUser,
|
|
8
12
|
getDbClient,
|
|
13
|
+
getKv,
|
|
9
14
|
getTaskAttachments,
|
|
10
15
|
getTaskById,
|
|
11
16
|
initDb,
|
|
17
|
+
startTask,
|
|
12
18
|
upsertKv,
|
|
13
19
|
} from "../be/db";
|
|
20
|
+
import { linkIdentity } from "../be/users";
|
|
14
21
|
import { resetFileStorageProviderForTests } from "../fs/registry";
|
|
15
22
|
import { drainBackground } from "../integrations/chat-sdk/background";
|
|
23
|
+
import { formatChatRelayText } from "../integrations/chat-sdk/relay";
|
|
16
24
|
import { CHAT_CHANNELS_NAMESPACE, routeChatMessage } from "../integrations/chat-sdk/router";
|
|
17
25
|
import { getChatInstance } from "../integrations/chat-sdk/runtime";
|
|
18
26
|
import { isChatThreadBuffered } from "../integrations/chat-sdk/thread-buffer";
|
|
19
27
|
import { chatSdkContextKey } from "../tasks/context-key";
|
|
28
|
+
import { registerDeferTaskTool } from "../tools/defer-task";
|
|
20
29
|
import type { Agent } from "../types";
|
|
30
|
+
import { NOT_TRANSCRIBED_NOTE, TRANSCRIPTION_ENV_KEYS } from "../utils/transcription";
|
|
21
31
|
import {
|
|
22
32
|
flushChatBuffers,
|
|
23
33
|
postChatWebhook,
|
|
@@ -30,7 +40,14 @@ import {
|
|
|
30
40
|
} from "./chat-sdk-harness";
|
|
31
41
|
|
|
32
42
|
const TEST_DB_PATH = "./test-chat-sdk-inbound.sqlite";
|
|
33
|
-
const ENV_KEYS = [
|
|
43
|
+
const ENV_KEYS = [
|
|
44
|
+
"AGENT_FS_LOCAL_DIR",
|
|
45
|
+
"AGENT_FS_API_URL",
|
|
46
|
+
"STEERING_ENABLED",
|
|
47
|
+
"TELEGRAM_MENTION_ON_REPLY",
|
|
48
|
+
"CHAT_SDK_OWNER_WINDOW_MIN",
|
|
49
|
+
...TRANSCRIPTION_ENV_KEYS,
|
|
50
|
+
];
|
|
34
51
|
const savedEnv = new Map(ENV_KEYS.map((key) => [key, process.env[key]]));
|
|
35
52
|
|
|
36
53
|
let harness: TelegramHarness;
|
|
@@ -68,6 +85,11 @@ beforeAll(async () => {
|
|
|
68
85
|
process.env.AGENT_FS_LOCAL_DIR = fsDir;
|
|
69
86
|
delete process.env.AGENT_FS_API_URL;
|
|
70
87
|
process.env.STEERING_ENABLED = "true";
|
|
88
|
+
// The adapter must treat a reply to the bot as a mention without the env fallback.
|
|
89
|
+
delete process.env.TELEGRAM_MENTION_ON_REPLY;
|
|
90
|
+
delete process.env.CHAT_SDK_OWNER_WINDOW_MIN;
|
|
91
|
+
// A provider key in the shell (OPENAI_API_KEY, ...) would switch transcription on.
|
|
92
|
+
for (const key of TRANSCRIPTION_ENV_KEYS) delete process.env[key];
|
|
71
93
|
resetFileStorageProviderForTests();
|
|
72
94
|
initDb(TEST_DB_PATH);
|
|
73
95
|
lead = await createAgent({
|
|
@@ -109,6 +131,16 @@ beforeEach(() => {
|
|
|
109
131
|
process.env.CHAT_SDK_BUFFER_MS = "60000";
|
|
110
132
|
});
|
|
111
133
|
|
|
134
|
+
type DeferTaskTool = { handler: (args: unknown, extra: unknown) => Promise<unknown> };
|
|
135
|
+
|
|
136
|
+
function buildDeferTaskTool(): DeferTaskTool {
|
|
137
|
+
const server = new McpServer({ name: "chat-sdk-inbound-test", version: "1.0.0" });
|
|
138
|
+
registerDeferTaskTool(server);
|
|
139
|
+
const tools = (server as unknown as { _registeredTools: Record<string, DeferTaskTool> })
|
|
140
|
+
._registeredTools;
|
|
141
|
+
return tools["defer-task"]!;
|
|
142
|
+
}
|
|
143
|
+
|
|
112
144
|
const dm = (id: number): TelegramChat => ({ id, type: "private" });
|
|
113
145
|
const group = (id: number): TelegramChat => ({ id, type: "group" });
|
|
114
146
|
|
|
@@ -134,7 +166,7 @@ describe("Chat SDK inbound pipeline", () => {
|
|
|
134
166
|
await postTelegramUpdate(telegramUpdate(dm(2002), "!now ship the hotfix"));
|
|
135
167
|
const tasks = await chatTasks(2002);
|
|
136
168
|
expect(tasks).toHaveLength(1);
|
|
137
|
-
expect(tasks[0]!.task).
|
|
169
|
+
expect(tasks[0]!.task).toEndWith("\n\nship the hotfix");
|
|
138
170
|
});
|
|
139
171
|
|
|
140
172
|
test("swarm#<uuid> routes to that agent", async () => {
|
|
@@ -143,7 +175,7 @@ describe("Chat SDK inbound pipeline", () => {
|
|
|
143
175
|
const tasks = await chatTasks(2003);
|
|
144
176
|
expect(tasks).toHaveLength(1);
|
|
145
177
|
expect(tasks[0]!.agentId).toBe(worker.id);
|
|
146
|
-
expect(tasks[0]!.task).
|
|
178
|
+
expect(tasks[0]!.task).toEndWith("\n\nbuild the parser");
|
|
147
179
|
});
|
|
148
180
|
|
|
149
181
|
test("a KV channel mapping routes to the pinned agent", async () => {
|
|
@@ -361,3 +393,564 @@ describe("Chat SDK inbound pipeline", () => {
|
|
|
361
393
|
expect(await chatTasks(-2009)).toHaveLength(0);
|
|
362
394
|
});
|
|
363
395
|
});
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Who asked and where. The harness posts every update straight to the
|
|
399
|
+
* webhook, which is what a bot with privacy mode off (or a group admin bot)
|
|
400
|
+
* receives. With privacy mode on Telegram delivers only a subset (commands,
|
|
401
|
+
* mentions, replies to the bot), so these filters hold there too.
|
|
402
|
+
*/
|
|
403
|
+
describe("Chat SDK requester and group addressing", () => {
|
|
404
|
+
const ACTOR = { kind: "system", id: "chat-sdk-inbound-test" } as const;
|
|
405
|
+
const supergroup = (id: number, title: string, extra: Record<string, unknown> = {}) => ({
|
|
406
|
+
chat: { id, type: "supergroup", title, ...extra },
|
|
407
|
+
});
|
|
408
|
+
const person = (id: number, first: string, username: string) => ({
|
|
409
|
+
from: { id, is_bot: false, first_name: first, username },
|
|
410
|
+
});
|
|
411
|
+
const ALICE = person(777_201, "Alice", "alice");
|
|
412
|
+
const BOB = person(777_202, "Bob", "bob");
|
|
413
|
+
const CAROL = person(777_203, "Carol", "carol");
|
|
414
|
+
|
|
415
|
+
async function steeringBodies(taskId: string): Promise<string[]> {
|
|
416
|
+
const rows = await getDbClient().query<{ body: string }>(
|
|
417
|
+
"SELECT body FROM task_steering_messages WHERE task_id = ? ORDER BY rowid",
|
|
418
|
+
[taskId],
|
|
419
|
+
);
|
|
420
|
+
return rows.map((row) => row.body);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function groupUpdate(chatId: number, text: string, sender: Record<string, unknown>) {
|
|
424
|
+
return telegramUpdate(group(chatId), text, { ...supergroup(chatId, "Life"), ...sender });
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
test("a registered user's chat task has requestedByUserId, and defer-task and the relay print their timezone", async () => {
|
|
428
|
+
const user = await createUser({ name: "Taras", timezone: "Europe/Madrid" });
|
|
429
|
+
await linkIdentity(user.id, "telegram", "777101", ACTOR);
|
|
430
|
+
await postTelegramUpdate(
|
|
431
|
+
telegramUpdate(dm(3001), "check the deploy", person(777_101, "Taras", "tarasyarema")),
|
|
432
|
+
);
|
|
433
|
+
await flushChatBuffers();
|
|
434
|
+
const [row] = await chatTasks(3001);
|
|
435
|
+
const task = await getTaskById(row!.id);
|
|
436
|
+
expect(task?.requestedByUserId).toBe(user.id);
|
|
437
|
+
|
|
438
|
+
await startTask(task!.id);
|
|
439
|
+
const tool = buildDeferTaskTool();
|
|
440
|
+
const result = (await tool.handler(
|
|
441
|
+
{ taskId: task!.id, delayMs: 60_000, summary: "deploy is rolling", note: "wait" },
|
|
442
|
+
{ sessionId: crypto.randomUUID(), requestInfo: { headers: { "x-agent-id": lead.id } } },
|
|
443
|
+
)) as { structuredContent: { success: boolean; nextRunAt?: string } };
|
|
444
|
+
expect(result.structuredContent.success).toBe(true);
|
|
445
|
+
const madrid = new Intl.DateTimeFormat("en-GB", {
|
|
446
|
+
timeZone: "Europe/Madrid",
|
|
447
|
+
hour: "2-digit",
|
|
448
|
+
minute: "2-digit",
|
|
449
|
+
hourCycle: "h23",
|
|
450
|
+
}).format(new Date(result.structuredContent.nextRunAt!));
|
|
451
|
+
const deferred = await getTaskById(task!.id);
|
|
452
|
+
expect(deferred?.output).toMatch(new RegExp(`at ${madrid}$`));
|
|
453
|
+
expect(deferred?.output).not.toContain("UTC");
|
|
454
|
+
expect(formatChatRelayText(deferred!)).toBe(deferred!.output!);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("an unknown sender gets no requester and lands in the Unmapped tracker", async () => {
|
|
458
|
+
await postTelegramUpdate(
|
|
459
|
+
telegramUpdate(dm(3002), "who am I", person(777_102, "Stranger", "stranger")),
|
|
460
|
+
);
|
|
461
|
+
await flushChatBuffers();
|
|
462
|
+
const [row] = await chatTasks(3002);
|
|
463
|
+
expect((await getTaskById(row!.id))?.requestedByUserId).toBeUndefined();
|
|
464
|
+
const meta = await getKv("integration:unmapped:telegram", "777102:meta");
|
|
465
|
+
expect(meta?.value).toMatchObject({ sampleEventType: "chat.message" });
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("the task description starts with the chat header", async () => {
|
|
469
|
+
await postTelegramUpdate(
|
|
470
|
+
telegramUpdate(group(-3003), "@swarm_test_bot plan the trip", {
|
|
471
|
+
...supergroup(-3003, "Life"),
|
|
472
|
+
...person(777_104, "Taras", "tarasyarema"),
|
|
473
|
+
}),
|
|
474
|
+
);
|
|
475
|
+
await flushChatBuffers();
|
|
476
|
+
const [plain] = await chatTasks(-3003);
|
|
477
|
+
expect(plain!.task).toStartWith(
|
|
478
|
+
'[chat] telegram · supergroup "Life" · from: Taras (@tarasyarema) · addressed: mention\n\n',
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
// A forum topic: the topic root is the bot's own message, which is the
|
|
482
|
+
// topic and not a reply to the bot.
|
|
483
|
+
await postTelegramUpdate(
|
|
484
|
+
telegramUpdate(group(-3004), "@swarm_test_bot book the flights", {
|
|
485
|
+
...supergroup(-3004, "Life", { is_forum: true }),
|
|
486
|
+
...person(777_104, "Taras", "tarasyarema"),
|
|
487
|
+
is_topic_message: true,
|
|
488
|
+
message_thread_id: 77,
|
|
489
|
+
reply_to_message: {
|
|
490
|
+
message_id: 77,
|
|
491
|
+
date: Math.floor(Date.now() / 1000),
|
|
492
|
+
chat: { id: -3004, type: "supergroup", title: "Life", is_forum: true },
|
|
493
|
+
from: { id: 4242, is_bot: true, first_name: "Swarm", username: "swarm_test_bot" },
|
|
494
|
+
forum_topic_created: { name: "Travel", icon_color: 0 },
|
|
495
|
+
},
|
|
496
|
+
}),
|
|
497
|
+
);
|
|
498
|
+
await flushChatBuffers();
|
|
499
|
+
const topicTasks = await getDbClient().query<{ task: string }>(
|
|
500
|
+
"SELECT task FROM agent_tasks WHERE source = 'chat' AND contextKey LIKE ?",
|
|
501
|
+
[`${chatSdkContextKey("telegram:-3004")}%`],
|
|
502
|
+
);
|
|
503
|
+
expect(topicTasks).toHaveLength(1);
|
|
504
|
+
expect(topicTasks[0]!.task).toStartWith(
|
|
505
|
+
'[chat] telegram · supergroup "Life" · forum topic "Travel" · from: Taras (@tarasyarema) · addressed: mention\n\n',
|
|
506
|
+
);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test("side chatter between two other people neither steers the running task nor creates one; the requester and a mention do", async () => {
|
|
510
|
+
const chatId = -3005;
|
|
511
|
+
await postTelegramUpdate(groupUpdate(chatId, "@swarm_test_bot draft the budget", ALICE));
|
|
512
|
+
await flushChatBuffers();
|
|
513
|
+
const [first] = await chatTasks(chatId);
|
|
514
|
+
await getDbClient().run("UPDATE agent_tasks SET status = 'in_progress' WHERE id = ?", [
|
|
515
|
+
first!.id,
|
|
516
|
+
]);
|
|
517
|
+
|
|
518
|
+
// Bob and Carol talk to each other.
|
|
519
|
+
await postTelegramUpdate(groupUpdate(chatId, "lunch at 2?", BOB));
|
|
520
|
+
await postTelegramUpdate(groupUpdate(chatId, "sure, the usual place", CAROL));
|
|
521
|
+
await flushChatBuffers();
|
|
522
|
+
expect(await chatTasks(chatId)).toHaveLength(1);
|
|
523
|
+
expect(await steeringBodies(first!.id)).toEqual([]);
|
|
524
|
+
|
|
525
|
+
// The requester's plain message, then Bob with a mention.
|
|
526
|
+
await postTelegramUpdate(groupUpdate(chatId, "include the rent", ALICE));
|
|
527
|
+
await postTelegramUpdate(groupUpdate(chatId, "@swarm_test_bot and the car", BOB));
|
|
528
|
+
await flushChatBuffers();
|
|
529
|
+
expect(await chatTasks(chatId)).toHaveLength(1);
|
|
530
|
+
expect(await steeringBodies(first!.id)).toEqual(["include the rent", "and the car"]);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("an unaddressed group message after the owner window creates no task", async () => {
|
|
534
|
+
const chatId = -3006;
|
|
535
|
+
await postTelegramUpdate(groupUpdate(chatId, "@swarm_test_bot book a table", ALICE));
|
|
536
|
+
await flushChatBuffers();
|
|
537
|
+
const [first] = await chatTasks(chatId);
|
|
538
|
+
const endedAt = (minutesAgo: number) =>
|
|
539
|
+
new Date(Date.now() - minutesAgo * 60_000).toISOString();
|
|
540
|
+
await getDbClient().run(
|
|
541
|
+
"UPDATE agent_tasks SET status = 'completed', finishedAt = ? WHERE id = ?",
|
|
542
|
+
[endedAt(60), first!.id],
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
// An hour after the task ended: even the requester's plain message is ignored.
|
|
546
|
+
await postTelegramUpdate(groupUpdate(chatId, "make it for four", ALICE));
|
|
547
|
+
await flushChatBuffers();
|
|
548
|
+
expect(await chatTasks(chatId)).toHaveLength(1);
|
|
549
|
+
|
|
550
|
+
// Control: inside the default 15-minute window the same message is a follow-up.
|
|
551
|
+
await getDbClient().run("UPDATE agent_tasks SET finishedAt = ? WHERE id = ?", [
|
|
552
|
+
endedAt(5),
|
|
553
|
+
first!.id,
|
|
554
|
+
]);
|
|
555
|
+
await postTelegramUpdate(groupUpdate(chatId, "make it for four", ALICE));
|
|
556
|
+
await flushChatBuffers();
|
|
557
|
+
const tasks = await chatTasks(chatId);
|
|
558
|
+
expect(tasks).toHaveLength(2);
|
|
559
|
+
expect(tasks[1]!.routingReason).toBe("continuity");
|
|
560
|
+
expect(tasks[1]!.task).toContain("addressed: requester-follow-up");
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test("a reply to the bot counts as a mention without TELEGRAM_MENTION_ON_REPLY", async () => {
|
|
564
|
+
expect(process.env.TELEGRAM_MENTION_ON_REPLY).toBeUndefined();
|
|
565
|
+
const chatId = -3007;
|
|
566
|
+
await postTelegramUpdate(
|
|
567
|
+
groupUpdate(chatId, "yes, go ahead", {
|
|
568
|
+
...BOB,
|
|
569
|
+
reply_to_message: {
|
|
570
|
+
message_id: 1,
|
|
571
|
+
date: Math.floor(Date.now() / 1000),
|
|
572
|
+
chat: { id: chatId, type: "supergroup", title: "Life" },
|
|
573
|
+
from: { id: 4242, is_bot: true, first_name: "Swarm", username: "swarm_test_bot" },
|
|
574
|
+
text: "Shall I send the invoice?",
|
|
575
|
+
},
|
|
576
|
+
}),
|
|
577
|
+
);
|
|
578
|
+
await flushChatBuffers();
|
|
579
|
+
const tasks = await chatTasks(chatId);
|
|
580
|
+
expect(tasks).toHaveLength(1);
|
|
581
|
+
expect(tasks[0]!.task).toStartWith(
|
|
582
|
+
'[chat] telegram · supergroup "Life" · from: Bob (@bob) · addressed: reply-to-bot · reply-to: "Shall I send the invoice?"\n\n',
|
|
583
|
+
);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// A supergroup reply carries `message_thread_id`: the id of the reply chain's
|
|
587
|
+
// first message. Replying to a bot message that starts a chain (a scheduled
|
|
588
|
+
// post, a chat-post) gives `message_thread_id` = the bot message's id, the
|
|
589
|
+
// same shape as a forum topic root, but `is_topic_message` is absent.
|
|
590
|
+
test("a supergroup reply to a bot message that starts a reply chain reaches the gate; chatter still drops", async () => {
|
|
591
|
+
const chatId = -3017;
|
|
592
|
+
const botPost = {
|
|
593
|
+
message_id: 55,
|
|
594
|
+
date: Math.floor(Date.now() / 1000),
|
|
595
|
+
chat: { id: chatId, type: "supergroup", title: "Life" },
|
|
596
|
+
from: { id: 4242, is_bot: true, first_name: "Swarm", username: "swarm_test_bot" },
|
|
597
|
+
text: "Calendar sync is done.",
|
|
598
|
+
};
|
|
599
|
+
const tasksIn = () =>
|
|
600
|
+
getDbClient().query<{ task: string }>(
|
|
601
|
+
"SELECT task FROM agent_tasks WHERE source = 'chat' AND contextKey LIKE ?",
|
|
602
|
+
[`${chatSdkContextKey(`telegram:${chatId}`)}%`],
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
// Control: an unaddressed message drops.
|
|
606
|
+
await postTelegramUpdate(groupUpdate(chatId, "anyone up for dinner Thursday?", BOB));
|
|
607
|
+
await flushChatBuffers();
|
|
608
|
+
expect(await tasksIn()).toHaveLength(0);
|
|
609
|
+
|
|
610
|
+
await postTelegramUpdate(
|
|
611
|
+
groupUpdate(chatId, "what's on tomorrow?", {
|
|
612
|
+
...ALICE,
|
|
613
|
+
message_thread_id: botPost.message_id,
|
|
614
|
+
reply_to_message: botPost,
|
|
615
|
+
}),
|
|
616
|
+
);
|
|
617
|
+
await flushChatBuffers();
|
|
618
|
+
const tasks = await tasksIn();
|
|
619
|
+
expect(tasks).toHaveLength(1);
|
|
620
|
+
expect(tasks[0]!.task).toStartWith(
|
|
621
|
+
'[chat] telegram · supergroup "Life" · from: Alice (@alice) · addressed: reply-to-bot · reply-to: "Calendar sync is done."\n\n',
|
|
622
|
+
);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test("an unknown sender in a known requester's thread does not inherit that requester", async () => {
|
|
626
|
+
const chatId = -3008;
|
|
627
|
+
const alice = await createUser({ name: "Alice", timezone: "Europe/Madrid" });
|
|
628
|
+
await linkIdentity(alice.id, "telegram", String(ALICE.from.id), ACTOR);
|
|
629
|
+
await postTelegramUpdate(groupUpdate(chatId, "@swarm_test_bot plan the offsite", ALICE));
|
|
630
|
+
await flushChatBuffers();
|
|
631
|
+
const [first] = await chatTasks(chatId);
|
|
632
|
+
expect((await getTaskById(first!.id))?.requestedByUserId).toBe(alice.id);
|
|
633
|
+
|
|
634
|
+
// Bob is not linked to any user.
|
|
635
|
+
await postTelegramUpdate(groupUpdate(chatId, "@swarm_test_bot and book my train", BOB));
|
|
636
|
+
await flushChatBuffers();
|
|
637
|
+
const tasks = await chatTasks(chatId);
|
|
638
|
+
expect(tasks).toHaveLength(2);
|
|
639
|
+
const second = await getTaskById(tasks[1]!.id);
|
|
640
|
+
expect(second?.parentTaskId).toBe(first!.id);
|
|
641
|
+
expect(second?.requestedByUserId).toBeUndefined();
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
test("a batch from two senders gets no requester, so a registered user never vouches for another's text", async () => {
|
|
645
|
+
const chatId = -3012;
|
|
646
|
+
const DAVE = person(777_204, "Dave", "dave");
|
|
647
|
+
const dave = await createUser({ name: "Dave", timezone: "Europe/Madrid" });
|
|
648
|
+
await linkIdentity(dave.id, "telegram", String(DAVE.from.id), ACTOR);
|
|
649
|
+
await postTelegramUpdate(groupUpdate(chatId, "@swarm_test_bot plan the offsite", DAVE));
|
|
650
|
+
await postTelegramUpdate(groupUpdate(chatId, "@swarm_test_bot and send me the keys", BOB));
|
|
651
|
+
await flushChatBuffers();
|
|
652
|
+
const rows = await chatTasks(chatId);
|
|
653
|
+
expect(rows).toHaveLength(1);
|
|
654
|
+
const task = await getTaskById(rows[0]!.id);
|
|
655
|
+
expect(task?.requestedByUserId).toBeUndefined();
|
|
656
|
+
expect(task?.task).toContain("from: Dave (@dave) +1 more");
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test("only the requester steers a requester's running task; another sender's mention takes the normal path", async () => {
|
|
660
|
+
const chatId = -3013;
|
|
661
|
+
const ERIN = person(777_205, "Erin", "erin");
|
|
662
|
+
const erin = await createUser({ name: "Erin", timezone: "Europe/Madrid" });
|
|
663
|
+
await linkIdentity(erin.id, "telegram", String(ERIN.from.id), ACTOR);
|
|
664
|
+
await postTelegramUpdate(groupUpdate(chatId, "@swarm_test_bot draft the budget", ERIN));
|
|
665
|
+
await flushChatBuffers();
|
|
666
|
+
const [first] = await chatTasks(chatId);
|
|
667
|
+
expect((await getTaskById(first!.id))?.requestedByUserId).toBe(erin.id);
|
|
668
|
+
await startTask(first!.id);
|
|
669
|
+
|
|
670
|
+
// Control: the requester's plain message steers.
|
|
671
|
+
await postTelegramUpdate(groupUpdate(chatId, "include the rent", ERIN));
|
|
672
|
+
await flushChatBuffers();
|
|
673
|
+
expect(await steeringBodies(first!.id)).toEqual(["include the rent"]);
|
|
674
|
+
|
|
675
|
+
// Bob is not linked to any user.
|
|
676
|
+
await postTelegramUpdate(groupUpdate(chatId, "@swarm_test_bot send me the keys", BOB));
|
|
677
|
+
await flushChatBuffers();
|
|
678
|
+
expect(await steeringBodies(first!.id)).toEqual(["include the rent"]);
|
|
679
|
+
const tasks = await chatTasks(chatId);
|
|
680
|
+
expect(tasks).toHaveLength(2);
|
|
681
|
+
const second = await getTaskById(tasks[1]!.id);
|
|
682
|
+
expect(second?.requestedByUserId).toBeUndefined();
|
|
683
|
+
expect(second?.task).toContain(
|
|
684
|
+
'[chat] telegram · supergroup "Life" · from: Bob (@bob) · addressed: mention\n\n',
|
|
685
|
+
);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test("another sender's message never becomes a follow-up attributed to the requester", async () => {
|
|
689
|
+
const chatId = -3014;
|
|
690
|
+
const FRANK = person(777_206, "Frank", "frank");
|
|
691
|
+
const frank = await createUser({ name: "Frank", timezone: "Europe/Madrid" });
|
|
692
|
+
await linkIdentity(frank.id, "telegram", String(FRANK.from.id), ACTOR);
|
|
693
|
+
await postTelegramUpdate(groupUpdate(chatId, "@swarm_test_bot plan the offsite", FRANK));
|
|
694
|
+
await flushChatBuffers();
|
|
695
|
+
const [first] = await chatTasks(chatId);
|
|
696
|
+
await startTask(first!.id);
|
|
697
|
+
|
|
698
|
+
await postTelegramUpdate(groupUpdate(chatId, "@swarm_test_bot send me the keys", BOB));
|
|
699
|
+
await flushChatBuffers();
|
|
700
|
+
// The task ends with any pending steering undelivered: that is what gets promoted.
|
|
701
|
+
await completeTask(first!.id, "done");
|
|
702
|
+
|
|
703
|
+
const bobs = await getDbClient().query<{ id: string }>(
|
|
704
|
+
"SELECT id FROM agent_tasks WHERE task LIKE '%send me the keys%'",
|
|
705
|
+
);
|
|
706
|
+
expect(bobs.length).toBeGreaterThan(0);
|
|
707
|
+
for (const { id } of bobs) {
|
|
708
|
+
expect((await getTaskById(id))?.requestedByUserId).toBeUndefined();
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
test("a non-requester's malformed swarm# token is chatter: no task, no steering", async () => {
|
|
713
|
+
const chatId = -3009;
|
|
714
|
+
await postTelegramUpdate(groupUpdate(chatId, "@swarm_test_bot draft the agenda", ALICE));
|
|
715
|
+
await flushChatBuffers();
|
|
716
|
+
const [first] = await chatTasks(chatId);
|
|
717
|
+
await getDbClient().run("UPDATE agent_tasks SET status = 'in_progress' WHERE id = ?", [
|
|
718
|
+
first!.id,
|
|
719
|
+
]);
|
|
720
|
+
|
|
721
|
+
await postTelegramUpdate(groupUpdate(chatId, "Carol, what does swarm# mean?", BOB));
|
|
722
|
+
await postTelegramUpdate(groupUpdate(chatId, "swarm#everyone is it a hashtag?", CAROL));
|
|
723
|
+
await flushChatBuffers();
|
|
724
|
+
expect(await chatTasks(chatId)).toHaveLength(1);
|
|
725
|
+
expect(await steeringBodies(first!.id)).toEqual([]);
|
|
726
|
+
|
|
727
|
+
// Control: once the task is done, a real token from a non-requester still routes.
|
|
728
|
+
await getDbClient().run(
|
|
729
|
+
"UPDATE agent_tasks SET status = 'completed', finishedAt = ? WHERE id = ?",
|
|
730
|
+
[new Date().toISOString(), first!.id],
|
|
731
|
+
);
|
|
732
|
+
await postTelegramUpdate(groupUpdate(chatId, `swarm#${worker.id} check the numbers`, BOB));
|
|
733
|
+
await flushChatBuffers();
|
|
734
|
+
const tasks = await chatTasks(chatId);
|
|
735
|
+
expect(tasks).toHaveLength(2);
|
|
736
|
+
expect(tasks[1]!.agentId).toBe(worker.id);
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
test("only a Telegram group expires the thread owner; DMs and other adapters keep continuity", async () => {
|
|
740
|
+
const finishedAt = new Date(Date.now() - 60 * 60_000).toISOString();
|
|
741
|
+
async function seedFinishedOwner(threadId: string): Promise<void> {
|
|
742
|
+
const task = await createTaskExtended("earlier ask", {
|
|
743
|
+
agentId: worker.id,
|
|
744
|
+
source: "chat",
|
|
745
|
+
contextKey: chatSdkContextKey(threadId),
|
|
746
|
+
});
|
|
747
|
+
await getDbClient().run(
|
|
748
|
+
"UPDATE agent_tasks SET status = 'completed', finishedAt = ? WHERE id = ?",
|
|
749
|
+
[finishedAt, task.id],
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
const continuity = {
|
|
753
|
+
kind: "agents",
|
|
754
|
+
matches: [
|
|
755
|
+
{
|
|
756
|
+
agent: expect.objectContaining({ id: worker.id }),
|
|
757
|
+
reason: "continuity",
|
|
758
|
+
matchedText: "thread follow-up",
|
|
759
|
+
},
|
|
760
|
+
],
|
|
761
|
+
};
|
|
762
|
+
const contexts = [
|
|
763
|
+
{ adapter: "telegram", channelId: "telegram:3101", threadId: "telegram:3101", dm: true },
|
|
764
|
+
{ adapter: "slack", channelId: "slack:C3101", threadId: "slack:C3101:1700000000.000100" },
|
|
765
|
+
{ adapter: "discord", channelId: "discord:g1:c3101", threadId: "discord:g1:c3101:t1" },
|
|
766
|
+
{ adapter: "teams", channelId: "teams:c3101", threadId: "teams:c3101:m1" },
|
|
767
|
+
];
|
|
768
|
+
for (const { dm: isDirectMessage = false, ...ctx } of contexts) {
|
|
769
|
+
await seedFinishedOwner(ctx.threadId);
|
|
770
|
+
expect(
|
|
771
|
+
await routeChatMessage("one more thing", { ...ctx, botMentioned: false, isDirectMessage }),
|
|
772
|
+
).toEqual(continuity);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Control: a Telegram group thread with the same history has no owner.
|
|
776
|
+
await seedFinishedOwner("telegram:-3101");
|
|
777
|
+
expect(
|
|
778
|
+
await routeChatMessage("one more thing", {
|
|
779
|
+
adapter: "telegram",
|
|
780
|
+
channelId: "telegram:-3101",
|
|
781
|
+
threadId: "telegram:-3101",
|
|
782
|
+
botMentioned: false,
|
|
783
|
+
isDirectMessage: false,
|
|
784
|
+
}),
|
|
785
|
+
).toBeNull();
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
describe("Chat SDK inbound voice notes and size limit", () => {
|
|
790
|
+
const voiceBytes = new Uint8Array([0x4f, 0x67, 0x67, 0x53, 0, 2, 0, 0, 1, 2, 3]);
|
|
791
|
+
const voiceUpdate = (chatId: number, fileId: string) =>
|
|
792
|
+
telegramUpdate(dm(chatId), "", {
|
|
793
|
+
voice: {
|
|
794
|
+
file_id: fileId,
|
|
795
|
+
file_unique_id: `${fileId}-unique`,
|
|
796
|
+
duration: 2,
|
|
797
|
+
mime_type: "audio/ogg",
|
|
798
|
+
file_size: voiceBytes.byteLength,
|
|
799
|
+
},
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
test("a voice note is transcribed into the task text and stays attached", async () => {
|
|
803
|
+
const requests: Array<{ auth: string | null; model: unknown; filename: string }> = [];
|
|
804
|
+
const stt = Bun.serve({
|
|
805
|
+
port: 0,
|
|
806
|
+
hostname: "127.0.0.1",
|
|
807
|
+
async fetch(req) {
|
|
808
|
+
const form = await req.formData();
|
|
809
|
+
const file = form.get("file") as File;
|
|
810
|
+
requests.push({
|
|
811
|
+
auth: req.headers.get("authorization"),
|
|
812
|
+
model: form.get("model"),
|
|
813
|
+
filename: file.name,
|
|
814
|
+
});
|
|
815
|
+
return Response.json({ text: "Deploy the staging branch tonight." });
|
|
816
|
+
},
|
|
817
|
+
});
|
|
818
|
+
process.env.TRANSCRIPTION_API_KEY = "stt-test-key";
|
|
819
|
+
process.env.TRANSCRIPTION_BASE_URL = `http://127.0.0.1:${stt.port}/v1`;
|
|
820
|
+
try {
|
|
821
|
+
harness.mock.addFile("voice/note-1.oga", voiceBytes);
|
|
822
|
+
await postTelegramUpdate(voiceUpdate(2101, "voice/note-1.oga"));
|
|
823
|
+
await flushChatBuffers();
|
|
824
|
+
} finally {
|
|
825
|
+
stt.stop(true);
|
|
826
|
+
delete process.env.TRANSCRIPTION_API_KEY;
|
|
827
|
+
delete process.env.TRANSCRIPTION_BASE_URL;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
expect(requests).toEqual([
|
|
831
|
+
{ auth: "Bearer stt-test-key", model: "whisper-1", filename: "audio-1.ogg" },
|
|
832
|
+
]);
|
|
833
|
+
const tasks = await chatTasks(2101);
|
|
834
|
+
expect(tasks).toHaveLength(1);
|
|
835
|
+
expect(tasks[0]!.task).toContain("[Transcript of audio-1]\nDeploy the staging branch tonight.");
|
|
836
|
+
const attachments = await getTaskAttachments(tasks[0]!.id);
|
|
837
|
+
expect(attachments).toHaveLength(1);
|
|
838
|
+
expect(attachments[0]).toMatchObject({
|
|
839
|
+
mimeType: "audio/ogg",
|
|
840
|
+
sizeBytes: voiceBytes.byteLength,
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
test("a short transcription key echoed in a provider or network error stays out of logs and task text", async () => {
|
|
845
|
+
const shortKey = "local-pass";
|
|
846
|
+
const stt = Bun.serve({
|
|
847
|
+
port: 0,
|
|
848
|
+
hostname: "127.0.0.1",
|
|
849
|
+
fetch: () =>
|
|
850
|
+
Response.json(
|
|
851
|
+
{
|
|
852
|
+
error: {
|
|
853
|
+
message: `Incorrect API key provided: ${shortKey}. Check your credentials.`,
|
|
854
|
+
},
|
|
855
|
+
},
|
|
856
|
+
{ status: 401 },
|
|
857
|
+
),
|
|
858
|
+
});
|
|
859
|
+
const realFetch = globalThis.fetch;
|
|
860
|
+
const warnings: string[] = [];
|
|
861
|
+
const warn = spyOn(console, "warn").mockImplementation((...args: unknown[]) => {
|
|
862
|
+
warnings.push(args.map(String).join(" "));
|
|
863
|
+
});
|
|
864
|
+
process.env.TRANSCRIPTION_API_KEY = shortKey;
|
|
865
|
+
process.env.TRANSCRIPTION_BASE_URL = `http://127.0.0.1:${stt.port}/v1`;
|
|
866
|
+
try {
|
|
867
|
+
harness.mock.addFile("voice/note-401.oga", voiceBytes);
|
|
868
|
+
await postTelegramUpdate(voiceUpdate(2111, "voice/note-401.oga"));
|
|
869
|
+
await flushChatBuffers();
|
|
870
|
+
|
|
871
|
+
// The network-error branch: the transport error echoes the key.
|
|
872
|
+
globalThis.fetch = Object.assign(
|
|
873
|
+
(input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) =>
|
|
874
|
+
String(input instanceof Request ? input.url : input).endsWith("/audio/transcriptions")
|
|
875
|
+
? Promise.reject(new TypeError(`connect failed, bearer ${shortKey} refused`))
|
|
876
|
+
: realFetch(input, init),
|
|
877
|
+
realFetch,
|
|
878
|
+
);
|
|
879
|
+
harness.mock.addFile("voice/note-net.oga", voiceBytes);
|
|
880
|
+
await postTelegramUpdate(voiceUpdate(2112, "voice/note-net.oga"));
|
|
881
|
+
await flushChatBuffers();
|
|
882
|
+
} finally {
|
|
883
|
+
globalThis.fetch = realFetch;
|
|
884
|
+
warn.mockRestore();
|
|
885
|
+
stt.stop(true);
|
|
886
|
+
delete process.env.TRANSCRIPTION_API_KEY;
|
|
887
|
+
delete process.env.TRANSCRIPTION_BASE_URL;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
for (const [chatId, failure] of [
|
|
891
|
+
[2111, "Incorrect API key provided: [REDACTED:TRANSCRIPTION_API_KEY]"],
|
|
892
|
+
[2112, "connect failed, bearer [REDACTED:TRANSCRIPTION_API_KEY] refused"],
|
|
893
|
+
] as const) {
|
|
894
|
+
const tasks = await chatTasks(chatId);
|
|
895
|
+
expect(tasks).toHaveLength(1);
|
|
896
|
+
expect(tasks[0]!.task).toContain(`(transcription failed: ${failure}`);
|
|
897
|
+
expect(tasks[0]!.task).not.toContain(shortKey);
|
|
898
|
+
expect(await getTaskAttachments(tasks[0]!.id)).toHaveLength(1);
|
|
899
|
+
}
|
|
900
|
+
const transcriptionWarnings = warnings.filter((line) => line.includes("could not transcribe"));
|
|
901
|
+
expect(transcriptionWarnings).toHaveLength(2);
|
|
902
|
+
expect(warnings.join("\n")).not.toContain(shortKey);
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
test("without a transcription provider the voice note is attached with a one-line note", async () => {
|
|
906
|
+
harness.mock.addFile("voice/note-2.oga", voiceBytes);
|
|
907
|
+
await postTelegramUpdate(voiceUpdate(2102, "voice/note-2.oga"));
|
|
908
|
+
await flushChatBuffers();
|
|
909
|
+
|
|
910
|
+
const tasks = await chatTasks(2102);
|
|
911
|
+
expect(tasks).toHaveLength(1);
|
|
912
|
+
expect(tasks[0]!.task).toContain(NOT_TRANSCRIBED_NOTE);
|
|
913
|
+
expect(await getTaskAttachments(tasks[0]!.id)).toHaveLength(1);
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
test("a 30 MB file is not downloaded; the task says why and the user gets a too-large reply", async () => {
|
|
917
|
+
const chatId = 2103;
|
|
918
|
+
await postTelegramUpdate(
|
|
919
|
+
telegramUpdate(dm(chatId), "", {
|
|
920
|
+
caption: "here is the recording",
|
|
921
|
+
document: {
|
|
922
|
+
file_id: "documents/big.zip",
|
|
923
|
+
file_unique_id: "big-unique",
|
|
924
|
+
file_name: "big.zip",
|
|
925
|
+
mime_type: "application/zip",
|
|
926
|
+
file_size: 30 * 1024 * 1024,
|
|
927
|
+
},
|
|
928
|
+
}),
|
|
929
|
+
);
|
|
930
|
+
await flushChatBuffers();
|
|
931
|
+
|
|
932
|
+
const tasks = await chatTasks(chatId);
|
|
933
|
+
expect(tasks).toHaveLength(1);
|
|
934
|
+
expect(tasks[0]!.task).toContain("big.zip (application/zip, 30.0 MB)");
|
|
935
|
+
expect(tasks[0]!.task).toContain(
|
|
936
|
+
"(not attached: too large (30.0 MB, the limit for files sent to the Telegram bot is 20.0 MB))",
|
|
937
|
+
);
|
|
938
|
+
expect(
|
|
939
|
+
harness.mock.calls.some(
|
|
940
|
+
(call) =>
|
|
941
|
+
call.method === "getFile" &&
|
|
942
|
+
(call.body as { file_id?: string }).file_id === "documents/big.zip",
|
|
943
|
+
),
|
|
944
|
+
).toBe(false);
|
|
945
|
+
const replies = harness.mock.calls
|
|
946
|
+
.filter((call) => /^send(Rich)?Message$/.test(call.method))
|
|
947
|
+
.map((call) => call.body as Record<string, unknown>)
|
|
948
|
+
.filter((body) => String(body.chat_id) === String(chatId))
|
|
949
|
+
.map((body) =>
|
|
950
|
+
String((body.rich_message as { markdown?: string } | undefined)?.markdown ?? body.text),
|
|
951
|
+
);
|
|
952
|
+
expect(replies).toHaveLength(1);
|
|
953
|
+
expect(replies[0]).toContain("Too large to read");
|
|
954
|
+
expect(replies[0]).toContain("`big.zip`: too large (30.0 MB");
|
|
955
|
+
});
|
|
956
|
+
});
|