@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
package/src/slack/assistant.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
fetchSlackFiles,
|
|
21
21
|
notifySlackFileFailures,
|
|
22
22
|
} from "./inbound-files";
|
|
23
|
+
import { shouldSkipLeftSlackThread } from "./left-threads";
|
|
23
24
|
import {
|
|
24
25
|
collectSlackMessageFiles,
|
|
25
26
|
type SlackMessageLike,
|
|
@@ -194,11 +195,26 @@ export function createAssistant(
|
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
|
|
198
|
+
const botMentioned = !!cachedBotUserId && rawText.includes(`<@${cachedBotUserId}>`);
|
|
199
|
+
// A thread the swarm left (slack-leave-thread) stays silent until someone
|
|
200
|
+
// @mentions the bot.
|
|
201
|
+
if (
|
|
202
|
+
await shouldSkipLeftSlackThread(
|
|
203
|
+
channelId,
|
|
204
|
+
msg.thread_ts as string | undefined,
|
|
205
|
+
botMentioned,
|
|
206
|
+
)
|
|
207
|
+
) {
|
|
208
|
+
console.log(
|
|
209
|
+
`[Slack] assistant: skipping message in left thread ${channelId}/${threadTs}`,
|
|
210
|
+
);
|
|
211
|
+
return ignoreSlackInbound("thread_left");
|
|
212
|
+
}
|
|
213
|
+
|
|
197
214
|
// If the message @-mentions someone OTHER than our bot and does NOT mention
|
|
198
215
|
// our bot, it is addressed to a different agent/user — do not spawn a task
|
|
199
216
|
// (unless SLACK_THREAD_WAKE_ON_ANY_MESSAGE=true).
|
|
200
217
|
if (cachedBotUserId) {
|
|
201
|
-
const botMentioned = rawText.includes(`<@${cachedBotUserId}>`);
|
|
202
218
|
if (!botMentioned && isAddressedToOtherUser(rawText, cachedBotUserId)) {
|
|
203
219
|
console.log(
|
|
204
220
|
`[Slack] assistant: skipping message in ${channelId}/${threadTs} — mentions another user, not us`,
|
package/src/slack/commands.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { App } from "@slack/bolt";
|
|
|
2
2
|
import { getAllAgents, getAllTasks } from "../be/db";
|
|
3
3
|
import { isEnvFlagEnabled } from "../utils/env-flag";
|
|
4
4
|
import { EXTERNAL_CHANNEL_BLOCKED_REPLY, shouldBlockChannel } from "./external-channel";
|
|
5
|
+
import { isSlackLeaveThreadDisabled } from "./left-threads";
|
|
5
6
|
|
|
6
7
|
export function registerCommandHandler(app: App): void {
|
|
7
8
|
app.command("/agent-swarm-status", async ({ ack, respond, command, client }) => {
|
|
@@ -88,9 +89,12 @@ export function registerCommandHandler(app: App): void {
|
|
|
88
89
|
• Follow-up tasks auto-depend on the active task in the thread for natural sequencing`);
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
const leaveLine = isSlackLeaveThreadDisabled()
|
|
93
|
+
? ""
|
|
94
|
+
: "\n• `!leave` in a thread - Make the swarm ignore that thread until someone @mentions it";
|
|
91
95
|
sections.push(`*Commands:*
|
|
92
96
|
• \`/agent-swarm-status\` - Show all agents and their current status
|
|
93
|
-
• \`/agent-swarm-help\` - Show this help message`);
|
|
97
|
+
• \`/agent-swarm-help\` - Show this help message${leaveLine}`);
|
|
94
98
|
|
|
95
99
|
const blocks = [
|
|
96
100
|
{
|
package/src/slack/handlers.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getTasksByAgentId,
|
|
11
11
|
isExtensionAgent,
|
|
12
12
|
} from "../be/db";
|
|
13
|
+
import { SteeringBlockedError } from "../be/steering";
|
|
13
14
|
import { dispatchPre } from "../extensions/dispatcher";
|
|
14
15
|
import { resolveTemplate } from "../prompts/resolver";
|
|
15
16
|
import { slackContextKey } from "../tasks/context-key";
|
|
@@ -51,6 +52,13 @@ import {
|
|
|
51
52
|
leaveExternalChannel,
|
|
52
53
|
shouldBlockChannel,
|
|
53
54
|
} from "./external-channel";
|
|
55
|
+
import {
|
|
56
|
+
isSlackLeaveThreadCommand,
|
|
57
|
+
isSlackLeaveThreadDisabled,
|
|
58
|
+
leaveSlackThread,
|
|
59
|
+
SLACK_LEFT_THREAD_CONFIRMATION,
|
|
60
|
+
shouldSkipLeftSlackThread,
|
|
61
|
+
} from "./left-threads";
|
|
54
62
|
import {
|
|
55
63
|
collectSlackMessageFiles,
|
|
56
64
|
extractSlackMessageText,
|
|
@@ -486,6 +494,46 @@ export function registerMessageHandler(app: App): void {
|
|
|
486
494
|
return ignoreSlackInbound("unauthorized_user");
|
|
487
495
|
}
|
|
488
496
|
|
|
497
|
+
// Check if bot was mentioned (in original text only)
|
|
498
|
+
const botMentioned = !!cachedBotUserId && !!msg.text?.includes(`<@${cachedBotUserId}>`);
|
|
499
|
+
|
|
500
|
+
// `!leave` in a thread the swarm is in: a human makes the swarm leave it,
|
|
501
|
+
// in code, with no workflow event, buffering, task, or model call.
|
|
502
|
+
// Any task bound to the thread counts, assigned or not, as does a thread
|
|
503
|
+
// the swarm started. Checked before the left-thread gate so `@bot !leave`
|
|
504
|
+
// leaves rather than rejoining. The confirmation is ephemeral, to the
|
|
505
|
+
// sender only.
|
|
506
|
+
if (
|
|
507
|
+
msg.thread_ts &&
|
|
508
|
+
!isSlackLeaveThreadDisabled() &&
|
|
509
|
+
isSlackLeaveThreadCommand(msg.text, cachedBotUserId) &&
|
|
510
|
+
((await getMostRecentTaskInThread(msg.channel, msg.thread_ts)) !== null ||
|
|
511
|
+
(await wasThreadStartedBySwarm(client, msg.channel, msg.thread_ts)))
|
|
512
|
+
) {
|
|
513
|
+
noteSlackInboundSideEffect("thread_leave");
|
|
514
|
+
await leaveSlackThread(msg.channel, msg.thread_ts, { slackUserId: msg.user });
|
|
515
|
+
console.log(`[Slack] Left thread ${msg.channel}/${msg.thread_ts}: !leave from ${msg.user}`);
|
|
516
|
+
try {
|
|
517
|
+
await client.chat.postEphemeral({
|
|
518
|
+
channel: msg.channel,
|
|
519
|
+
user: msg.user,
|
|
520
|
+
thread_ts: msg.thread_ts,
|
|
521
|
+
text: SLACK_LEFT_THREAD_CONFIRMATION,
|
|
522
|
+
});
|
|
523
|
+
} catch (error) {
|
|
524
|
+
console.error("[Slack] Failed to confirm !leave:", error);
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// A thread the swarm left (slack-leave-thread or `!leave`) stays silent until someone
|
|
530
|
+
// @mentions the bot. Checked before the workflow event, buffering, routing,
|
|
531
|
+
// or any AI call.
|
|
532
|
+
if (await shouldSkipLeftSlackThread(msg.channel, msg.thread_ts, botMentioned)) {
|
|
533
|
+
console.log(`[Slack] Skipping message in left thread ${msg.channel}/${msg.thread_ts}`);
|
|
534
|
+
return ignoreSlackInbound("thread_left");
|
|
535
|
+
}
|
|
536
|
+
|
|
489
537
|
// Resolve canonical user identity via the three-step cascade:
|
|
490
538
|
// fast-path alias lookup → enrich+auto-link by email → unmapped tracker.
|
|
491
539
|
// Returns undefined when no email is recoverable; task creation proceeds
|
|
@@ -519,9 +567,6 @@ export function registerMessageHandler(app: App): void {
|
|
|
519
567
|
}
|
|
520
568
|
const botUserId = cachedBotUserId;
|
|
521
569
|
|
|
522
|
-
// Check if bot was mentioned (in original text only)
|
|
523
|
-
const botMentioned = !!msg.text?.includes(`<@${botUserId}>`);
|
|
524
|
-
|
|
525
570
|
// Detect assistant thread context. Bolt's Assistant middleware claims DM thread
|
|
526
571
|
// messages with no subtype or `file_share` (`isAssistantMessage` in @slack/bolt), so
|
|
527
572
|
// those — file uploads included — are handled in assistant.ts. An assistant-thread
|
|
@@ -862,6 +907,7 @@ export function registerMessageHandler(app: App): void {
|
|
|
862
907
|
message: taskDescription,
|
|
863
908
|
messageTimestamps: [msg.ts],
|
|
864
909
|
requestedByUserId,
|
|
910
|
+
slackUserId: msg.user,
|
|
865
911
|
})
|
|
866
912
|
: null;
|
|
867
913
|
if (steering) {
|
|
@@ -937,12 +983,14 @@ export function registerMessageHandler(app: App): void {
|
|
|
937
983
|
} catch (error) {
|
|
938
984
|
// A policy block is an answered outcome; anything else is a failure the
|
|
939
985
|
// user sees as "could not assign" and the dispatcher must not call success.
|
|
940
|
-
|
|
986
|
+
const blocked =
|
|
987
|
+
error instanceof TaskCreationBlockedError || error instanceof SteeringBlockedError;
|
|
988
|
+
if (!blocked) {
|
|
941
989
|
reportSlackInboundFailure("task_creation_error");
|
|
942
990
|
}
|
|
943
991
|
results.failed.push({
|
|
944
992
|
agentName: agent.name,
|
|
945
|
-
reason:
|
|
993
|
+
reason: blocked ? error.reason : "error",
|
|
946
994
|
});
|
|
947
995
|
}
|
|
948
996
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { deleteKv, getKv, upsertKv } from "../be/db";
|
|
2
|
+
import { isEnvFlagEnabled } from "../utils/env-flag";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Slack threads the swarm left, via the `slack-leave-thread` tool or a
|
|
6
|
+
* human's `!leave` in the thread.
|
|
7
|
+
*
|
|
8
|
+
* A left thread drops every inbound message that does not @mention the bot,
|
|
9
|
+
* in code, before buffering, routing, or any AI call. A mention clears the
|
|
10
|
+
* state and the message is handled as usual. Stored in kv_entries so a
|
|
11
|
+
* restart keeps it.
|
|
12
|
+
*/
|
|
13
|
+
export const SLACK_LEFT_THREAD_KV_NAMESPACE = "slack-left-thread";
|
|
14
|
+
|
|
15
|
+
/** Operator flag that turns leaving threads off: the tool and `!leave`. */
|
|
16
|
+
export const SLACK_LEAVE_THREAD_DISABLE = "SLACK_LEAVE_THREAD_DISABLE";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read at call time, not cached: a global swarm_config row is hydrated into
|
|
20
|
+
* process.env on every config reload, so a flip applies without a restart.
|
|
21
|
+
*/
|
|
22
|
+
export function isSlackLeaveThreadDisabled(): boolean {
|
|
23
|
+
return isEnvFlagEnabled(SLACK_LEAVE_THREAD_DISABLE, false);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Ephemeral reply to the human who sent `!leave`. */
|
|
27
|
+
export const SLACK_LEFT_THREAD_CONFIRMATION =
|
|
28
|
+
"Left this thread. @mention the swarm to bring it back.";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* True when a thread message is the `!leave` command: exactly `!leave`,
|
|
32
|
+
* optionally after @mentions of the bot. A message that mentions anyone else
|
|
33
|
+
* is not addressed to the swarm and does not match.
|
|
34
|
+
*/
|
|
35
|
+
export function isSlackLeaveThreadCommand(
|
|
36
|
+
text: string | undefined,
|
|
37
|
+
botUserId: string | null,
|
|
38
|
+
): boolean {
|
|
39
|
+
const withoutBotMentions = botUserId
|
|
40
|
+
? (text ?? "").replaceAll(`<@${botUserId}>`, "")
|
|
41
|
+
: (text ?? "");
|
|
42
|
+
return withoutBotMentions.trim() === "!leave";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function threadKey(channelId: string, threadTs: string): string {
|
|
46
|
+
return `${channelId}:${threadTs}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Who left the thread: an agent via the tool, or a Slack user via `!leave`. */
|
|
50
|
+
export type SlackThreadLeftBy = { agentId: string } | { slackUserId: string };
|
|
51
|
+
|
|
52
|
+
export async function leaveSlackThread(
|
|
53
|
+
channelId: string,
|
|
54
|
+
threadTs: string,
|
|
55
|
+
leftBy: SlackThreadLeftBy,
|
|
56
|
+
): Promise<void> {
|
|
57
|
+
await upsertKv({
|
|
58
|
+
namespace: SLACK_LEFT_THREAD_KV_NAMESPACE,
|
|
59
|
+
key: threadKey(channelId, threadTs),
|
|
60
|
+
value: { ...leftBy, leftAt: new Date().toISOString() },
|
|
61
|
+
valueType: "json",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function isSlackThreadLeft(channelId: string, threadTs: string): Promise<boolean> {
|
|
66
|
+
return (await getKv(SLACK_LEFT_THREAD_KV_NAMESPACE, threadKey(channelId, threadTs))) !== null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Inbound gate for left threads. Returns true when the message must be dropped:
|
|
71
|
+
* the thread is left and the message does not @mention the bot. A mention
|
|
72
|
+
* clears the left state and returns false.
|
|
73
|
+
*/
|
|
74
|
+
export async function shouldSkipLeftSlackThread(
|
|
75
|
+
channelId: string,
|
|
76
|
+
threadTs: string | undefined,
|
|
77
|
+
botMentioned: boolean,
|
|
78
|
+
): Promise<boolean> {
|
|
79
|
+
if (!threadTs) return false;
|
|
80
|
+
const key = threadKey(channelId, threadTs);
|
|
81
|
+
if (botMentioned) {
|
|
82
|
+
if (await deleteKv(SLACK_LEFT_THREAD_KV_NAMESPACE, key)) {
|
|
83
|
+
console.log(`[Slack] Rejoined left thread ${key}: bot mentioned`);
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return isSlackThreadLeft(channelId, threadTs);
|
|
88
|
+
}
|
package/src/slack/steering.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface SlackThreadSteeringRequest {
|
|
|
9
9
|
message: string;
|
|
10
10
|
messageTimestamps?: string[];
|
|
11
11
|
requestedByUserId?: string;
|
|
12
|
+
slackUserId?: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export interface SlackThreadSteeringResult {
|
|
@@ -52,6 +53,8 @@ export async function requestSlackThreadSteering(
|
|
|
52
53
|
source: "slack",
|
|
53
54
|
createdByKind: "user",
|
|
54
55
|
createdByUserId: args.requestedByUserId,
|
|
56
|
+
messageTimestamps: args.messageTimestamps,
|
|
57
|
+
slack: { channelId: args.channelId, threadTs: args.threadTs, userId: args.slackUserId },
|
|
55
58
|
});
|
|
56
59
|
for (const messageTs of args.messageTimestamps ?? []) {
|
|
57
60
|
await createLogEntry({
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
getLeadAgent,
|
|
5
5
|
getMostRecentTaskInThread,
|
|
6
6
|
} from "../be/db";
|
|
7
|
+
import { SteeringBlockedError } from "../be/steering";
|
|
7
8
|
import { findUserByExternalId } from "../be/users";
|
|
8
9
|
import { createAdditiveBuffer } from "../tasks/additive-buffer";
|
|
9
10
|
import { slackContextKey } from "../tasks/context-key";
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
fetchSlackFiles,
|
|
22
23
|
notifySlackFileFailures,
|
|
23
24
|
} from "./inbound-files";
|
|
25
|
+
import { isSlackThreadLeft } from "./left-threads";
|
|
24
26
|
import { extractSlackMessageText } from "./message-text";
|
|
25
27
|
import { ensureSlackThreadTree, isSlackRenderV2Enabled } from "./render-v2";
|
|
26
28
|
import { getAgentDisplayName, getAgentSlackIcon } from "./responses";
|
|
@@ -164,6 +166,12 @@ async function slackFlush(
|
|
|
164
166
|
return;
|
|
165
167
|
}
|
|
166
168
|
const { channelId, threadTs } = split;
|
|
169
|
+
// The thread was left (slack-leave-thread) while these replies waited. Only
|
|
170
|
+
// non-mentions are buffered, so none of them rejoins it.
|
|
171
|
+
if (await isSlackThreadLeft(channelId, threadTs)) {
|
|
172
|
+
console.log(`[Slack] Buffer flush: dropping ${items.length} message(s) in left thread ${key}`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
167
175
|
// Buffer is guaranteed to have at least one item — the first carries the
|
|
168
176
|
// original requester's userId (same semantics as the pre-refactor version).
|
|
169
177
|
const originalRequesterId = items[0]!.userId;
|
|
@@ -226,22 +234,38 @@ async function slackFlush(
|
|
|
226
234
|
|
|
227
235
|
// Steering carries text only. As on the mention path, files need a follow-up
|
|
228
236
|
// task so its attachments exist before a worker can claim it.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
237
|
+
let steering: Awaited<ReturnType<typeof requestSlackThreadSteering>>;
|
|
238
|
+
try {
|
|
239
|
+
steering =
|
|
240
|
+
files.length === 0
|
|
241
|
+
? await requestSlackThreadSteering({
|
|
242
|
+
channelId,
|
|
243
|
+
threadTs,
|
|
244
|
+
message:
|
|
245
|
+
nextSenderIndex === -1
|
|
246
|
+
? combinedText
|
|
247
|
+
: await rewriteSlackMentions(
|
|
248
|
+
steeringItems.map((item) => item.text).join("\n---\n"),
|
|
249
|
+
botUserId,
|
|
250
|
+
),
|
|
251
|
+
messageTimestamps: steeringItems.map((item) => item.ts),
|
|
252
|
+
requestedByUserId,
|
|
253
|
+
slackUserId: originalRequesterId,
|
|
254
|
+
})
|
|
255
|
+
: null;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (!(error instanceof SteeringBlockedError)) throw error;
|
|
258
|
+
console.info("[Slack] pre.task.steer blocked:", error.extension.name, error.reason);
|
|
259
|
+
if (app) {
|
|
260
|
+
for (const item of steeringItems) {
|
|
261
|
+
await ackSlackMessage(app.client, channelId, item.ts, "no_entry");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (nextSenderIndex !== -1) {
|
|
265
|
+
await slackFlush(items.slice(nextSenderIndex), key, immediate);
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
245
269
|
if (steering) {
|
|
246
270
|
console.log(
|
|
247
271
|
`[Slack] Buffer flushed → steering ${steering.result.outcome} for task ${steering.task.id}`,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Follow-up re-delegation guard, shared by every agent dispatch path that can
|
|
3
|
+
* run inside a follow-up session: MCP `send-task` and durable-run
|
|
4
|
+
* `ctx.step.agentTask` steps (`src/http/script-runs.ts`).
|
|
5
|
+
*
|
|
6
|
+
* When the dispatching session's source task is a `follow-up` in a Slack
|
|
7
|
+
* thread, two rules stop the cycle
|
|
8
|
+
* worker completes → follow-up → Lead re-delegates → repeat,
|
|
9
|
+
* while a distinct next step (step 2 of a chain, a fix round, a retry) passes:
|
|
10
|
+
*
|
|
11
|
+
* 1. same-work: the new task matches a completed task in the thread under the
|
|
12
|
+
* dedup similarity rules. Bypassed when a task in the thread was cancelled
|
|
13
|
+
* or failed after that completion: the work was interrupted or went wrong,
|
|
14
|
+
* so re-dispatch is the correct response, not a deduped no-op.
|
|
15
|
+
* 2. loop-cap: the caller already made FOLLOW_UP_DISPATCH_CAP dispatches into
|
|
16
|
+
* the thread within FOLLOW_UP_DISPATCH_WINDOW_MINUTES. A durable script run
|
|
17
|
+
* counts once however many steps it dispatched. Independent of similarity,
|
|
18
|
+
* so a loop that rewords each re-delegation still stops.
|
|
19
|
+
*
|
|
20
|
+
* Callers bypass both with `allowRedelegation` and log the override. Each
|
|
21
|
+
* caller evaluates the rules inside the write transaction that inserts the
|
|
22
|
+
* task, so two concurrent dispatches into one thread (send-task or a run's
|
|
23
|
+
* first step, in any mix) cannot both read the count below the cap.
|
|
24
|
+
*/
|
|
25
|
+
import {
|
|
26
|
+
countAgentDispatchesInThread,
|
|
27
|
+
findCompletedTasksInThread,
|
|
28
|
+
findRecentCancelledTaskInThread,
|
|
29
|
+
findRecentFailedTaskInThread,
|
|
30
|
+
getTaskById,
|
|
31
|
+
} from "../be/db";
|
|
32
|
+
import { type DedupMatch, matchSimilarTask } from "../tools/task-dedup";
|
|
33
|
+
|
|
34
|
+
/** How far back the same-work rule looks for a completed task in the thread. */
|
|
35
|
+
export const FOLLOW_UP_SAME_WORK_WINDOW_MINUTES = 2880; // 48 hours
|
|
36
|
+
/**
|
|
37
|
+
* Loop cap: dispatches one agent may make into one Slack thread per window
|
|
38
|
+
* before a follow-up session needs allowRedelegation. A long legitimate chain
|
|
39
|
+
* (plan, implement, review, fix, re-review, fix, CI fix, retry) is about 8
|
|
40
|
+
* dispatches; a runaway loop makes one per worker run (minutes), so it hits
|
|
41
|
+
* 10 within a few hours and burns at most 10 worker sessions.
|
|
42
|
+
*/
|
|
43
|
+
export const FOLLOW_UP_DISPATCH_CAP = 10;
|
|
44
|
+
export const FOLLOW_UP_DISPATCH_WINDOW_MINUTES = 360; // 6 hours
|
|
45
|
+
|
|
46
|
+
/** Rows per page when the same-work rule scans a thread's completions. */
|
|
47
|
+
export const FOLLOW_UP_SAME_WORK_PAGE_SIZE = 200;
|
|
48
|
+
|
|
49
|
+
export type FollowUpThread = { channelId: string; threadTs: string; userId?: string };
|
|
50
|
+
|
|
51
|
+
export type FollowUpRedelegationBlock = {
|
|
52
|
+
rule: "same-work" | "loop-cap";
|
|
53
|
+
message: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The Slack thread the guard applies to, or null when the source task is not
|
|
58
|
+
* a `follow-up` in a Slack thread. A `resume` source is intentionally exempt:
|
|
59
|
+
* a resume IS the legitimate re-dispatch.
|
|
60
|
+
*/
|
|
61
|
+
export async function followUpThreadForSourceTask(
|
|
62
|
+
sourceTaskId: string | null | undefined,
|
|
63
|
+
): Promise<FollowUpThread | null> {
|
|
64
|
+
if (!sourceTaskId) return null;
|
|
65
|
+
const sourceTask = await getTaskById(sourceTaskId);
|
|
66
|
+
if (
|
|
67
|
+
sourceTask?.taskType !== "follow-up" ||
|
|
68
|
+
!sourceTask.slackChannelId ||
|
|
69
|
+
!sourceTask.slackThreadTs
|
|
70
|
+
) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
channelId: sourceTask.slackChannelId,
|
|
75
|
+
threadTs: sourceTask.slackThreadTs,
|
|
76
|
+
userId: sourceTask.slackUserId,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The most recent completed task in the thread's same-work window whose
|
|
82
|
+
* description matches this dispatch. Pages through every completion in the
|
|
83
|
+
* window (an older match can sit behind any number of newer unrelated ones),
|
|
84
|
+
* one bounded query per page, and stops at the first match.
|
|
85
|
+
*/
|
|
86
|
+
async function findMatchingCompletedWork(args: {
|
|
87
|
+
channelId: string;
|
|
88
|
+
threadTs: string;
|
|
89
|
+
taskDescription: string;
|
|
90
|
+
targetAgentId?: string;
|
|
91
|
+
}): Promise<DedupMatch | null> {
|
|
92
|
+
let before: { lastUpdatedAt: string; id: string } | undefined;
|
|
93
|
+
for (;;) {
|
|
94
|
+
const page = await findCompletedTasksInThread(
|
|
95
|
+
args.channelId,
|
|
96
|
+
args.threadTs,
|
|
97
|
+
FOLLOW_UP_SAME_WORK_WINDOW_MINUTES,
|
|
98
|
+
FOLLOW_UP_SAME_WORK_PAGE_SIZE,
|
|
99
|
+
before,
|
|
100
|
+
);
|
|
101
|
+
for (const existing of page) {
|
|
102
|
+
const match = matchSimilarTask(existing, args);
|
|
103
|
+
if (match) return match;
|
|
104
|
+
}
|
|
105
|
+
const last = page.at(-1);
|
|
106
|
+
if (!last || page.length < FOLLOW_UP_SAME_WORK_PAGE_SIZE) return null;
|
|
107
|
+
before = { lastUpdatedAt: last.lastUpdatedAt, id: last.id };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The follow-up re-delegation rules. Returns the rule that blocks this
|
|
113
|
+
* dispatch, or null when it may proceed.
|
|
114
|
+
*/
|
|
115
|
+
export async function evaluateFollowUpRedelegation(args: {
|
|
116
|
+
channelId: string;
|
|
117
|
+
threadTs: string;
|
|
118
|
+
creatorAgentId: string;
|
|
119
|
+
taskDescription: string;
|
|
120
|
+
targetAgentId?: string;
|
|
121
|
+
}): Promise<FollowUpRedelegationBlock | null> {
|
|
122
|
+
const { channelId, threadTs } = args;
|
|
123
|
+
const match = await findMatchingCompletedWork(args);
|
|
124
|
+
if (match) {
|
|
125
|
+
const existing = match.task;
|
|
126
|
+
const [cancelled, failed] = await Promise.all([
|
|
127
|
+
findRecentCancelledTaskInThread(channelId, threadTs, FOLLOW_UP_SAME_WORK_WINDOW_MINUTES),
|
|
128
|
+
findRecentFailedTaskInThread(channelId, threadTs, FOLLOW_UP_SAME_WORK_WINDOW_MINUTES),
|
|
129
|
+
]);
|
|
130
|
+
const completedAt = new Date(existing.lastUpdatedAt).getTime();
|
|
131
|
+
const interruptedLater = [cancelled, failed].some(
|
|
132
|
+
(t) => t && new Date(t.lastUpdatedAt).getTime() > completedAt,
|
|
133
|
+
);
|
|
134
|
+
if (!interruptedLater) {
|
|
135
|
+
return {
|
|
136
|
+
rule: "same-work",
|
|
137
|
+
message: `Blocked by the follow-up re-delegation guard (same-work rule): this task matches completed task ${existing.id.slice(0, 8)} in this Slack thread (${match.reason}), so the work was already done. If it must run again, pass allowRedelegation: true.`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const dispatched = await countAgentDispatchesInThread(
|
|
143
|
+
channelId,
|
|
144
|
+
threadTs,
|
|
145
|
+
args.creatorAgentId,
|
|
146
|
+
FOLLOW_UP_DISPATCH_WINDOW_MINUTES,
|
|
147
|
+
);
|
|
148
|
+
if (dispatched >= FOLLOW_UP_DISPATCH_CAP) {
|
|
149
|
+
return {
|
|
150
|
+
rule: "loop-cap",
|
|
151
|
+
message: `Blocked by the follow-up re-delegation guard (loop-cap rule): you already dispatched ${dispatched} times into this Slack thread in the last ${FOLLOW_UP_DISPATCH_WINDOW_MINUTES / 60} hours (cap ${FOLLOW_UP_DISPATCH_CAP}; a durable script run counts once). If this is a deliberate next step and not a re-delegation loop, pass allowRedelegation: true.`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
@@ -1179,6 +1179,85 @@ ${serveFakeAcpAgent("FakeCopilot")}
|
|
|
1179
1179
|
expect(toAcpMcpServers(null)).toEqual([]);
|
|
1180
1180
|
});
|
|
1181
1181
|
|
|
1182
|
+
test("a target without the opt-in hooks keeps its session/new payload, MCP URL and no steering", async () => {
|
|
1183
|
+
const cwd = makeTempDir();
|
|
1184
|
+
const agentPath = join(cwd, "fake-acp-hooks-off.ts");
|
|
1185
|
+
const captureFile = join(cwd, "session-new.json");
|
|
1186
|
+
await Bun.write(
|
|
1187
|
+
agentPath,
|
|
1188
|
+
`
|
|
1189
|
+
import { Readable, Writable } from "node:stream";
|
|
1190
|
+
import { PROTOCOL_VERSION, agent, methods, ndJsonStream } from "${ACP_SDK_PATH}";
|
|
1191
|
+
class FakeAgent {
|
|
1192
|
+
constructor(connection) { this.connection = connection; }
|
|
1193
|
+
async initialize() {
|
|
1194
|
+
// Advertises every capability the opt-in hooks key off; a target without
|
|
1195
|
+
// the hooks must ignore them.
|
|
1196
|
+
return {
|
|
1197
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1198
|
+
agentCapabilities: {
|
|
1199
|
+
loadSession: true,
|
|
1200
|
+
mcpCapabilities: { http: true, sse: false },
|
|
1201
|
+
sessionCapabilities: { resume: {}, systemPrompt: {} },
|
|
1202
|
+
_meta: { fx: { steering: true } },
|
|
1203
|
+
},
|
|
1204
|
+
agentInfo: { name: "fake", version: "9.9.9" },
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
async newSession(params) {
|
|
1208
|
+
await Bun.write(${JSON.stringify(captureFile)}, JSON.stringify(params));
|
|
1209
|
+
return { sessionId: "hooks-off-1" };
|
|
1210
|
+
}
|
|
1211
|
+
async prompt() { return { stopReason: "end_turn" }; }
|
|
1212
|
+
async cancel() {}
|
|
1213
|
+
}
|
|
1214
|
+
${serveFakeAcpAgent("FakeAgent")}
|
|
1215
|
+
`,
|
|
1216
|
+
);
|
|
1217
|
+
const { close: closeTokenStub, apiUrl } = await startTokenStubServer(
|
|
1218
|
+
"hooks-off-token",
|
|
1219
|
+
"aseph_hooksofftoken1234567890",
|
|
1220
|
+
);
|
|
1221
|
+
// A reachable, plain-HTTP, non-loopback URL: only requiresSecureMcpUrl rewrites it.
|
|
1222
|
+
const publicStyleUrl = apiUrl.replace("127.0.0.1", "0.0.0.0");
|
|
1223
|
+
try {
|
|
1224
|
+
const adapter = new ACPAdapter();
|
|
1225
|
+
const session = await adapter.createSession(
|
|
1226
|
+
baseConfig({
|
|
1227
|
+
cwd,
|
|
1228
|
+
apiUrl: publicStyleUrl,
|
|
1229
|
+
resumeSessionId: "ignored-without-a-resume-hook",
|
|
1230
|
+
env: {
|
|
1231
|
+
PATH: process.env.PATH ?? "",
|
|
1232
|
+
HOME: process.env.HOME ?? "",
|
|
1233
|
+
ACP_TARGET_COMMAND: "bun",
|
|
1234
|
+
ACP_TARGET_ARGS: JSON.stringify([agentPath]),
|
|
1235
|
+
},
|
|
1236
|
+
}),
|
|
1237
|
+
);
|
|
1238
|
+
const events: ProviderEvent[] = [];
|
|
1239
|
+
session.onEvent((event) => events.push(event));
|
|
1240
|
+
await session.waitForCompletion();
|
|
1241
|
+
|
|
1242
|
+
expect(session.sessionId).toBe("hooks-off-1");
|
|
1243
|
+
expect(session.deliverSteering).toBeUndefined();
|
|
1244
|
+
expect(await adapter.canResume("hooks-off-1")).toBe(false);
|
|
1245
|
+
const params = JSON.parse(await Bun.file(captureFile).text());
|
|
1246
|
+
expect(Object.keys(params).sort()).toEqual(["cwd", "mcpServers"]);
|
|
1247
|
+
expect(params.mcpServers.map((server: { url: string }) => server.url)).toEqual([
|
|
1248
|
+
`${publicStyleUrl}/mcp`,
|
|
1249
|
+
]);
|
|
1250
|
+
// Every ACP target records the agent's own version on session_init.
|
|
1251
|
+
expect(events.find((event) => event.type === "session_init")).toMatchObject({
|
|
1252
|
+
sessionId: "hooks-off-1",
|
|
1253
|
+
provider: "acp",
|
|
1254
|
+
harnessVariantMeta: { version: "9.9.9" },
|
|
1255
|
+
});
|
|
1256
|
+
} finally {
|
|
1257
|
+
await closeTokenStub();
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1182
1261
|
test("completes before the revoke response and logs a failed revocation", async () => {
|
|
1183
1262
|
const cwd = makeTempDir();
|
|
1184
1263
|
const agentPath = join(cwd, "fake-acp-ephem-agent.ts");
|
|
@@ -1290,6 +1290,18 @@ describe("getBasePrompt: harness tool discovery", () => {
|
|
|
1290
1290
|
expect(result).toContain('import { store_progress } from "agent-swarm"');
|
|
1291
1291
|
});
|
|
1292
1292
|
|
|
1293
|
+
test("fx gets capability_search guidance with mcp_swarm_ tool names", async () => {
|
|
1294
|
+
const result = await getBasePrompt({
|
|
1295
|
+
...minimalArgs,
|
|
1296
|
+
provider: "fx",
|
|
1297
|
+
traits: { ...localTraits, hasToolSearch: false },
|
|
1298
|
+
});
|
|
1299
|
+
expect(result).not.toContain(deferred);
|
|
1300
|
+
expect(result).not.toContain("already in your tool list");
|
|
1301
|
+
expect(result).toContain("`capability_search`");
|
|
1302
|
+
expect(result).toContain("`store-progress` is `mcp_swarm_store-progress`");
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1293
1305
|
test("codex gets exec guidance with the normalized tool name", async () => {
|
|
1294
1306
|
const result = await getBasePrompt({
|
|
1295
1307
|
...minimalArgs,
|
|
@@ -12,13 +12,16 @@ import {
|
|
|
12
12
|
createUser,
|
|
13
13
|
getDbClient,
|
|
14
14
|
getKv,
|
|
15
|
+
getLogsByTaskId,
|
|
15
16
|
getTaskAttachments,
|
|
16
17
|
getTaskById,
|
|
17
18
|
initDb,
|
|
18
19
|
startTask,
|
|
19
20
|
upsertKv,
|
|
20
21
|
} from "../be/db";
|
|
22
|
+
import { installExtension } from "../be/extensions/db";
|
|
21
23
|
import { linkIdentity } from "../be/users";
|
|
24
|
+
import { enableExtension, stopExtensionRuntime } from "../extensions/lifecycle";
|
|
22
25
|
import { resetFileStorageProviderForTests } from "../fs/registry";
|
|
23
26
|
import { drainBackground } from "../integrations/chat-sdk/background";
|
|
24
27
|
import { CHAT_CHANNEL_SETTINGS_NAMESPACE } from "../integrations/chat-sdk/reactions";
|
|
@@ -42,6 +45,8 @@ import {
|
|
|
42
45
|
telegramUpdate,
|
|
43
46
|
} from "./chat-sdk-harness";
|
|
44
47
|
|
|
48
|
+
import { loadBundleFixture } from "./fixtures/extensions/load";
|
|
49
|
+
|
|
45
50
|
const TEST_DB_PATH = "./test-chat-sdk-inbound.sqlite";
|
|
46
51
|
const ENV_KEYS = [
|
|
47
52
|
"AGENT_FS_LOCAL_DIR",
|
|
@@ -271,6 +276,31 @@ describe("Chat SDK inbound pipeline", () => {
|
|
|
271
276
|
expect(sendCount()).toBe(sendsBefore);
|
|
272
277
|
});
|
|
273
278
|
|
|
279
|
+
test("an extension-blocked steer never falls back to a follow-up task", async () => {
|
|
280
|
+
await postTelegramUpdate(telegramUpdate(group(-2025), "@swarm_test_bot plan the report"));
|
|
281
|
+
await flushChatBuffers();
|
|
282
|
+
const [task] = await chatTasks(-2025);
|
|
283
|
+
expect(task).toBeDefined();
|
|
284
|
+
await startTask(task!.id);
|
|
285
|
+
const bundle = await loadBundleFixture("steering-policy");
|
|
286
|
+
const { extension } = await installExtension({ ...bundle, config: { action: "block" } });
|
|
287
|
+
await enableExtension(extension.id);
|
|
288
|
+
try {
|
|
289
|
+
const blocked = telegramUpdate(group(-2025), "Do not deliver this correction");
|
|
290
|
+
await postTelegramUpdate(blocked);
|
|
291
|
+
await flushChatBuffers();
|
|
292
|
+
expect(await chatTasks(-2025)).toHaveLength(1);
|
|
293
|
+
expect(await steeringRows(task!.id)).toEqual([]);
|
|
294
|
+
expect(reactionsOn(-2025, blocked)).toEqual(["👎"]);
|
|
295
|
+
expect(
|
|
296
|
+
(await getLogsByTaskId(task!.id)).filter((log) => log.eventType === "task_steering"),
|
|
297
|
+
).toEqual([]);
|
|
298
|
+
} finally {
|
|
299
|
+
await stopExtensionRuntime();
|
|
300
|
+
await getDbClient().run("DELETE FROM extensions WHERE id = ?", [extension.id]);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
274
304
|
test("a channel that opts into text acks also gets the steering reply", async () => {
|
|
275
305
|
await upsertKv({
|
|
276
306
|
namespace: CHAT_CHANNEL_SETTINGS_NAMESPACE,
|