@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.
Files changed (326) hide show
  1. package/README.md +1 -1
  2. package/dist/acp-adapter-bp91d37f.js +15 -0
  3. package/dist/{actions-gfrr9vb7.js → actions-yj0eeaqh.js} +19 -18
  4. package/dist/{amp-adapter-0s58gm46.js → amp-adapter-4pg9h34q.js} +3 -3
  5. package/dist/{app-stvq692g.js → app-909e97cr.js} +3 -3
  6. package/dist/{assistant-5s37fr9n.js → assistant-qdyb5fyy.js} +32 -23
  7. package/dist/{boot-reembed-yzy1841n.js → boot-reembed-fk0s9z18.js} +1 -1
  8. package/dist/{boot-reembed-cv82gz7y.js → boot-reembed-gs1rzrnh.js} +1 -1
  9. package/dist/{boot-scrub-sweep-8k4q95ap.js → boot-scrub-sweep-4qfd4kfx.js} +4 -4
  10. package/dist/{boot-seeds-ftt9htah.js → boot-seeds-yr00k13d.js} +4 -4
  11. package/dist/{boxd-515v1t8z.js → boxd-vdmfssd9.js} +5 -5
  12. package/dist/{claude-adapter-t00wrzeh.js → claude-adapter-qta1f1xn.js} +7 -7
  13. package/dist/{claude-managed-adapter-g6vmjccb.js → claude-managed-adapter-dvs2xw5n.js} +2 -2
  14. package/dist/{claude-managed-setup-afpa82f7.js → claude-managed-setup-sc6j4dm0.js} +3 -3
  15. package/dist/{claude-sdk-session-vj0c2azx.js → claude-sdk-session-zgrz6yvc.js} +9175 -9076
  16. package/dist/{cli-gjr2y2xk.js → cli-0agcv1v5.js} +4 -1
  17. package/dist/{cli-edmm9d9y.js → cli-0at8w34w.js} +7 -7
  18. package/dist/{cli-a81qevyg.js → cli-0qatpy8q.js} +20 -36
  19. package/dist/{cli-bcz074zx.js → cli-0yv5z2pg.js} +1 -1
  20. package/dist/{cli-a4btqqft.js → cli-1b7btd38.js} +1 -1
  21. package/dist/{cli-a2jw6ee5.js → cli-1p89xny0.js} +1 -1
  22. package/dist/{cli-v726afdm.js → cli-2pnswgh8.js} +1 -1
  23. package/dist/{cli-jkt52mgt.js → cli-386tyxxg.js} +2 -2
  24. package/dist/{cli-az0bpf8p.js → cli-3ypk9vk1.js} +3 -3
  25. package/dist/{cli-wk9md5px.js → cli-436532ck.js} +4 -4
  26. package/dist/{cli-7n5sbw0z.js → cli-4necwtsv.js} +5 -5
  27. package/dist/cli-54t00gt8.js +151 -0
  28. package/dist/{cli-655xm9ms.js → cli-583frbyr.js} +1 -1
  29. package/dist/{cli-4r12t6w6.js → cli-5c4hjzne.js} +101 -4
  30. package/dist/{cli-hf6zt5m6.js → cli-69szn7w7.js} +17 -1
  31. package/dist/{cli-rtk9g7av.js → cli-73jzwksr.js} +6 -2
  32. package/dist/{cli-jg0nfda6.js → cli-75gkpjvw.js} +52 -21
  33. package/dist/{cli-wcmnan2g.js → cli-89knwags.js} +5 -5
  34. package/dist/{cli-pwyevs29.js → cli-8bc1w0tw.js} +22 -21
  35. package/dist/{cli-t282jgab.js → cli-8snvgs5r.js} +41 -16
  36. package/dist/{cli-vqgnyh76.js → cli-98rw5dt5.js} +15 -3
  37. package/dist/{cli-9cctx1tn.js → cli-a22h291q.js} +1 -1
  38. package/dist/{cli-ycm6180s.js → cli-a7v854rf.js} +5 -3
  39. package/dist/{cli-98pke4vk.js → cli-ae7mrdf9.js} +2 -2
  40. package/dist/{cli-je8yeyc5.js → cli-bjb3z51z.js} +5 -5
  41. package/dist/{cli-mc97xy2s.js → cli-c60vetmm.js} +16 -4
  42. package/dist/{cli-j4kw3rsx.js → cli-cm5yqn35.js} +18 -0
  43. package/dist/{cli-wndv9p7g.js → cli-decfb6v8.js} +6 -6
  44. package/dist/{cli-h55y2bjc.js → cli-dhar9qay.js} +2 -2
  45. package/dist/{cli-8yy525ad.js → cli-drr5v6av.js} +2 -2
  46. package/dist/{cli-5gr1g711.js → cli-dxae2yg7.js} +2 -2
  47. package/dist/{cli-pe09p6dv.js → cli-e3r2neck.js} +8 -7
  48. package/dist/{cli-13jvmew3.js → cli-ekwks2vs.js} +7 -7
  49. package/dist/{cli-jg4p5t36.js → cli-es6va8jk.js} +121 -51
  50. package/dist/{cli-j4kt1mpb.js → cli-faxe6thg.js} +3 -3
  51. package/dist/{cli-tzrpv5sc.js → cli-ftyxyspd.js} +1 -1
  52. package/dist/cli-fw2w0kva.js +47 -0
  53. package/dist/{cli-2dfbv8qr.js → cli-fy6x31ps.js} +1 -0
  54. package/dist/{cli-aq58fk5m.js → cli-g83s9ewh.js} +7 -1
  55. package/dist/cli-gdhpcbhf.js +55 -0
  56. package/dist/{cli-82jg7fp9.js → cli-he5nxaa8.js} +1 -1
  57. package/dist/{cli-8veytvbh.js → cli-he9wchme.js} +1 -1
  58. package/dist/{cli-k76tgq03.js → cli-hgaazk69.js} +2 -2
  59. package/dist/{cli-d2pzqej1.js → cli-hr2e2fft.js} +741 -7
  60. package/dist/{cli-dae4sbgy.js → cli-jm2vfpfd.js} +2 -2
  61. package/dist/{cli-1s1dqn1j.js → cli-k2gzwsj2.js} +297 -140
  62. package/dist/{cli-b1afgtj3.js → cli-kqdg1bsb.js} +3 -3
  63. package/dist/cli-m2z8jagn.js +12078 -0
  64. package/dist/{cli-mvckssyq.js → cli-m48g6p95.js} +5 -5
  65. package/dist/{cli-bwty35gx.js → cli-mthwvjre.js} +1 -1
  66. package/dist/{cli-41z8xwj0.js → cli-nhrvpaj7.js} +34 -8
  67. package/dist/{cli-ahd90bwt.js → cli-nwj0ee2w.js} +6 -6
  68. package/dist/{cli-kvtc7aez.js → cli-p5gp8pcx.js} +44 -14
  69. package/dist/{cli-avgcz4ra.js → cli-pd7vagab.js} +436 -171
  70. package/dist/{cli-zn15c46z.js → cli-q0k1w5zd.js} +1 -1
  71. package/dist/{cli-bq2pcphn.js → cli-rmmmrjkd.js} +1 -1
  72. package/dist/{cli-2qpvmmkw.js → cli-s0qnd4wv.js} +1 -1
  73. package/dist/{cli-kg8hchyg.js → cli-sa02jsqt.js} +1 -1
  74. package/dist/{cli-1yekbyqm.js → cli-shpbx5ee.js} +283 -82
  75. package/dist/{cli-9y354g3j.js → cli-srye3vdt.js} +1 -1
  76. package/dist/{cli-byqfbnjw.js → cli-svj5090z.js} +9 -9
  77. package/dist/{cli-gcbdjt47.js → cli-vfym7wmz.js} +6 -6
  78. package/dist/{cli-v1yw3wha.js → cli-vvpx3gsx.js} +4 -4
  79. package/dist/{cli-a1fhj4n9.js → cli-w6dt9ged.js} +2 -2
  80. package/dist/{cli-1gm8jcyr.js → cli-waqbxg08.js} +5232 -68
  81. package/dist/{cli-5hjced2v.js → cli-wp9sp1qb.js} +6 -6
  82. package/dist/{cli-eny54typ.js → cli-wx3rhnm0.js} +1 -1
  83. package/dist/{cli-s0exq74r.js → cli-xbbndvjy.js} +3 -3
  84. package/dist/{cli-f255yaqn.js → cli-xds5f77d.js} +3 -3
  85. package/dist/{cli-0sbkss7h.js → cli-xnqgpshy.js} +2 -2
  86. package/dist/{cli-0fgr1w9b.js → cli-xnrwazav.js} +20 -3
  87. package/dist/{cli-gf5hbmdr.js → cli-yjdgea32.js} +3 -3
  88. package/dist/{cli-cq1pf336.js → cli-yn4kxs06.js} +124 -11
  89. package/dist/{cli-224epwwf.js → cli-yx4kfngt.js} +3 -3
  90. package/dist/{cli-eqx6k993.js → cli-yy3rhsdw.js} +47 -13
  91. package/dist/{cli-36g23z21.js → cli-zbbwez0r.js} +4 -4
  92. package/dist/{cli-ktmvb20s.js → cli-zm72anhg.js} +12 -12
  93. package/dist/cli.js +26 -25
  94. package/dist/{codex-adapter-g0gctw29.js → codex-adapter-ayd1fb6h.js} +5 -5
  95. package/dist/{codex-hook-vrzz2xhr.js → codex-hook-9efm2x1m.js} +1 -1
  96. package/dist/{codex-login-0gg46ygn.js → codex-login-y0pmff1c.js} +1 -1
  97. package/dist/{codex-session-runner-dr2cxe1c.js → codex-session-runner-jesa32xc.js} +6 -6
  98. package/dist/{commands-1p07cvff.js → commands-hanewgf2.js} +9 -3
  99. package/dist/{controller-z0s7sv6r.js → controller-dt1n3c1q.js} +9 -4
  100. package/dist/{controllers-connect-x9kvvhyk.js → controllers-connect-xx8x64n5.js} +22 -21
  101. package/dist/{cursor-adapter-m5b98pvk.js → cursor-adapter-9s4dwwdw.js} +2 -2
  102. package/dist/{db-capk2raj.js → db-3vttcbv4.js} +3 -3
  103. package/dist/{db-rsh04kh3.js → db-9dv0g1ab.js} +12 -4
  104. package/dist/{devin-adapter-yxm87fe9.js → devin-adapter-r9phbnc9.js} +1 -1
  105. package/dist/{devtools-82fzqra8.js → devtools-qxfgbsqz.js} +1 -1
  106. package/dist/{dispatcher-304q6fwa.js → dispatcher-tx9hd1sv.js} +18 -17
  107. package/dist/{docker-7rxt0rry.js → docker-c1a3gqmf.js} +91 -24
  108. package/dist/{dsh-adapter-btewem0y.js → dsh-adapter-007y4ejs.js} +2 -2
  109. package/dist/{e2b-renc0k8q.js → e2b-a19d28rz.js} +4 -4
  110. package/dist/{e2b-7fc9b9bd.js → e2b-we6hs6q5.js} +6 -6
  111. package/dist/{easter-egg-3d-fm1p3n4d.js → easter-egg-3d-rgbrqb8s.js} +1 -1
  112. package/dist/{embedding-space-s68v49ct.js → embedding-space-ny1e2940.js} +6 -6
  113. package/dist/{embeddings-4ms3604w.js → embeddings-2t386t13.js} +1 -1
  114. package/dist/{event-bus-dfjx2qdh.js → event-bus-g0ka3pbw.js} +1 -1
  115. package/dist/{execute-mcdgccjs.js → execute-pksh0ceh.js} +1 -1
  116. package/dist/{external-channel-9dxwfztn.js → external-channel-phgmhmd7.js} +2 -2
  117. package/dist/fx-adapter-fy8rr921.js +18 -0
  118. package/dist/{google-generative-ai-yvmdxyxa.js → google-generative-ai-8y3m3k4b.js} +1 -1
  119. package/dist/{google-vertex-jrtdh9gs.js → google-vertex-w3v98n7b.js} +1 -1
  120. package/dist/{grok-adapter-05ecv4h0.js → grok-adapter-q0abvpaj.js} +8 -8
  121. package/dist/handlers-by3sfxyx.js +32 -0
  122. package/dist/{heartbeat-3n94heh3.js → heartbeat-kadyek8p.js} +21 -20
  123. package/dist/{hook-aadrn69x.js → hook-5zs3msf6.js} +6 -6
  124. package/dist/{hook-zxwje86c.js → hook-9k551xnj.js} +4 -4
  125. package/dist/{http-xzxk9mst.js → http-mhz9vw1f.js} +272 -103
  126. package/dist/index-4f2pvxae.js +37 -0
  127. package/dist/index-98gz7tmp.js +30 -0
  128. package/dist/{index-5rk6rfzr.js → index-fvzke5kx.js} +1 -1
  129. package/dist/index-k08z2cak.js +33 -0
  130. package/dist/{index-yqhz88yy.js → index-pxn30mse.js} +8 -8
  131. package/dist/{index-d34f6r87.js → index-q361cg2f.js} +1 -1
  132. package/dist/{index-9j4zwcht.js → index-rvr00sg7.js} +168 -675
  133. package/dist/{index-qeb82z37.js → index-vdygbq10.js} +1 -1
  134. package/dist/{keepalive-w4aakbyk.js → keepalive-w56f32cg.js} +4 -4
  135. package/dist/lead-enq35nqp.js +70 -0
  136. package/dist/{maintenance-hbptpk6p.js → maintenance-7mdgxcea.js} +1 -1
  137. package/dist/{oauth-refresh-sweep-217semrv.js → oauth-refresh-sweep-29txy9vk.js} +3 -3
  138. package/dist/{onboard-vz2s5df3.js → onboard-kpsrkarj.js} +5 -5
  139. package/dist/{opencode-adapter-gtfd7d7v.js → opencode-adapter-j25vm3rr.js} +5 -5
  140. package/dist/{otel-impl-aefave37.js → otel-impl-6kkct7a8.js} +2 -2
  141. package/dist/{pi-mono-adapter-f8h5yseh.js → pi-mono-adapter-exhkzhn2.js} +117 -23
  142. package/dist/{pricing-refresh-m1yhh9ya.js → pricing-refresh-n2x7jh6k.js} +3 -3
  143. package/dist/{rbac-roles-rrw8twdz.js → rbac-roles-81ypx6es.js} +1 -1
  144. package/dist/{rbac-roles-svj72cdf.js → rbac-roles-b5htkjby.js} +2 -2
  145. package/dist/{render-v2-amwbnfxh.js → render-v2-qn6jmpk4.js} +3 -3
  146. package/dist/{resume-9cd6wf6z.js → resume-wd30wx23.js} +7 -7
  147. package/dist/{runtime-fp4vrx7m.js → runtime-4hsg7h60.js} +1 -1
  148. package/dist/{session-access-21t6yjyg.js → session-access-5qvfevhk.js} +4 -4
  149. package/dist/{session-source-backfill-pxbzcsc6.js → session-source-backfill-n1czavbr.js} +3 -3
  150. package/dist/{setup-4592tqz4.js → setup-dnb56qy5.js} +3 -3
  151. package/dist/{virtual-modules-gzfnpmnz.js → virtual-modules-76xxv9fs.js} +2 -2
  152. package/dist/{work-objects-t9tcf174.js → work-objects-4n21tfkm.js} +21 -20
  153. package/dist/{worker-follow-up-q32jh4yk.js → worker-follow-up-77hvzxww.js} +18 -17
  154. package/dist/worker-gg1576j3.js +70 -0
  155. package/dist/{x-b6wxntdt.js → x-vmmye1kb.js} +2 -2
  156. package/openapi.json +189 -32
  157. package/package.json +8 -7
  158. package/src/be/db/script-runs.ts +23 -0
  159. package/src/be/db/session-summaries.ts +8 -2
  160. package/src/be/db/session-tree.ts +174 -61
  161. package/src/be/db/session-visibility.ts +5 -4
  162. package/src/be/db/tasks/read.ts +122 -3
  163. package/src/be/db-client.ts +8 -5
  164. package/src/be/db.ts +23 -8
  165. package/src/be/memory/providers/sqlite-store.ts +49 -12
  166. package/src/be/migrations/259_session_summaries_open_tasks.sql +111 -0
  167. package/src/be/migrations/260_runtime_pool_cache.sql +20 -0
  168. package/src/be/migrations/262_session_visibility_indexes.sql +18 -0
  169. package/src/be/migrations/263_task_list_filter_index.sql +11 -0
  170. package/src/be/migrations/264_slack_thread_cover_index.sql +9 -0
  171. package/src/be/migrations/266_script_runs_source_task.sql +11 -0
  172. package/src/be/model-validation.ts +5 -1
  173. package/src/be/pricing-normalize.ts +7 -2
  174. package/src/be/runtime-pools/cache-cleanup.ts +109 -0
  175. package/src/be/runtime-pools/db.ts +65 -6
  176. package/src/be/runtime-pools/remote-controller.ts +5 -0
  177. package/src/be/runtime-pools/supervisor.ts +33 -2
  178. package/src/be/scripts/typecheck.ts +23 -0
  179. package/src/be/seed/schedules-seeder.ts +135 -9
  180. package/src/be/seed/workflows-seeder.ts +65 -1
  181. package/src/be/seed-pricing.ts +23 -0
  182. package/src/be/seed-scripts/catalog/task-failure-audit.ts +5 -1
  183. package/src/be/steering.ts +50 -3
  184. package/src/be/swarm-config-guard.ts +1 -0
  185. package/src/commands/controller.ts +4 -0
  186. package/src/commands/entrypoint-api-readiness.ts +74 -0
  187. package/src/commands/provider-credentials.ts +29 -2
  188. package/src/commands/runner.ts +86 -5
  189. package/src/extensions/contract-types.generated.ts +1 -1
  190. package/src/extensions/contract.ts +27 -1
  191. package/src/extensions/dispatcher.ts +4 -0
  192. package/src/http/agents.ts +1 -0
  193. package/src/http/index.ts +1 -1
  194. package/src/http/list-total-cache.ts +14 -7
  195. package/src/http/mcp-bridge.ts +4 -0
  196. package/src/http/memory.ts +38 -9
  197. package/src/http/script-runs.ts +174 -5
  198. package/src/http/status.ts +10 -2
  199. package/src/http/tasks.ts +4 -2
  200. package/src/integrations/chat-sdk/inbound.ts +15 -5
  201. package/src/integrations/chat-sdk/steering.ts +3 -1
  202. package/src/kv-reserved-namespaces.ts +2 -0
  203. package/src/prompts/base-prompt.ts +4 -1
  204. package/src/prompts/session-templates.ts +26 -0
  205. package/src/providers/acp-adapter.ts +190 -26
  206. package/src/providers/acp-targets.ts +306 -6
  207. package/src/providers/amp-adapter.ts +1 -1
  208. package/src/providers/codex-adapter.ts +9 -44
  209. package/src/providers/codex-oauth/flow.ts +24 -3
  210. package/src/providers/codex-oauth/storage.ts +56 -17
  211. package/src/providers/codex-oauth/subscription.ts +67 -0
  212. package/src/providers/fx-adapter.ts +211 -0
  213. package/src/providers/index.ts +5 -1
  214. package/src/providers/mcp-loopback-proxy.ts +94 -0
  215. package/src/providers/pi-mono-adapter.ts +98 -13
  216. package/src/providers/pricing-sources.md +8 -0
  217. package/src/providers/reasoning-effort.ts +17 -1
  218. package/src/providers/types.ts +2 -1
  219. package/src/runtime-providers/controller-protocol.ts +14 -1
  220. package/src/runtime-providers/controller.ts +5 -0
  221. package/src/runtime-providers/docker.ts +108 -23
  222. package/src/runtime-providers/types.ts +16 -0
  223. package/src/scheduler/scheduler.ts +36 -3
  224. package/src/script-workflows/workflow-ctx.ts +8 -0
  225. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  226. package/src/scripts-runtime/types/stdlib.d.ts +27 -0
  227. package/src/scripts-runtime/types/swarm-extension.d.ts +33 -0
  228. package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -0
  229. package/src/server-user.ts +1 -0
  230. package/src/server.ts +7 -0
  231. package/src/slack/assistant.ts +17 -1
  232. package/src/slack/commands.ts +5 -1
  233. package/src/slack/handlers.ts +53 -5
  234. package/src/slack/left-threads.ts +88 -0
  235. package/src/slack/steering.ts +3 -0
  236. package/src/slack/thread-buffer.ts +40 -16
  237. package/src/tasks/follow-up-redelegation-guard.ts +155 -0
  238. package/src/tests/acp-adapter.test.ts +79 -0
  239. package/src/tests/base-prompt.test.ts +12 -0
  240. package/src/tests/chat-sdk-inbound.test.ts +30 -0
  241. package/src/tests/chatgpt-subscription-model-groups.test.ts +62 -0
  242. package/src/tests/chatgpt-subscription-pi.test.ts +170 -0
  243. package/src/tests/chatgpt-subscription-session-bench.test.ts +198 -0
  244. package/src/tests/claude-artifacts-read-script.test.ts +209 -0
  245. package/src/tests/codex-oauth-credential-info.test.ts +40 -0
  246. package/src/tests/codex-oauth-refresh-egress.test.ts +194 -0
  247. package/src/tests/context-window.test.ts +7 -0
  248. package/src/tests/db-client.test.ts +21 -0
  249. package/src/tests/db-task-read-contract.test.ts +8 -2
  250. package/src/tests/db-task-write-contract.test.ts +5 -1
  251. package/src/tests/entrypoint-api-readiness.test.ts +416 -210
  252. package/src/tests/explicit-model-validation.test.ts +113 -8
  253. package/src/tests/extensions-run-context.test.ts +11 -0
  254. package/src/tests/extensions-steering.test.ts +353 -0
  255. package/src/tests/extensions-validate.test.ts +5 -0
  256. package/src/tests/fixtures/extensions/steering-policy/hooks.ts +39 -0
  257. package/src/tests/fixtures/extensions/steering-policy/manifest.json +7 -0
  258. package/src/tests/fixtures/fake-fx.ts +167 -0
  259. package/src/tests/fixtures/fx/acp-responses-recorded.json +157 -0
  260. package/src/tests/fixtures/fx/acp-updates-recorded.jsonl +15 -0
  261. package/src/tests/follow-up-redelivery-guard.test.ts +432 -202
  262. package/src/tests/fx-adapter.test.ts +771 -0
  263. package/src/tests/grok-adapter.test.ts +36 -0
  264. package/src/tests/list-total-cache.test.ts +27 -2
  265. package/src/tests/mcp-loopback-proxy.test.ts +193 -0
  266. package/src/tests/memory-fts-populate.test.ts +11 -9
  267. package/src/tests/memory-retrieval-purge.test.ts +80 -0
  268. package/src/tests/migration-260-runtime-pool-cache.test.ts +118 -0
  269. package/src/tests/model-tier-resolution.test.ts +3 -3
  270. package/src/tests/pricing-refresh.test.ts +5 -5
  271. package/src/tests/prompt-template-session.test.ts +5 -3
  272. package/src/tests/provider-registration.test.ts +2 -1
  273. package/src/tests/query-audit-plans.test.ts +30 -2
  274. package/src/tests/runtime-cache-cleanup.test.ts +367 -0
  275. package/src/tests/runtime-e2b-pools.test.ts +41 -0
  276. package/src/tests/runtime-pools-db.test.ts +98 -0
  277. package/src/tests/runtime-pools-http.test.ts +29 -0
  278. package/src/tests/runtime-providers-docker.test.ts +253 -0
  279. package/src/tests/runtime-remote-controller.test.ts +66 -2
  280. package/src/tests/runtime-supervisor.test.ts +66 -2
  281. package/src/tests/script-connections.test.ts +13 -0
  282. package/src/tests/script-runs-http.test.ts +684 -2
  283. package/src/tests/seed-model-profiles.test.ts +2 -2
  284. package/src/tests/seed-schedules.test.ts +138 -0
  285. package/src/tests/seed-scripts.test.ts +26 -0
  286. package/src/tests/session-cost-recompute-unit.test.ts +39 -0
  287. package/src/tests/session-costs-golden.test.ts +26 -0
  288. package/src/tests/session-summaries-plans.test.ts +120 -0
  289. package/src/tests/session-summaries.test.ts +72 -12
  290. package/src/tests/sessions-tree.test.ts +11 -3
  291. package/src/tests/slack-leave-thread.test.ts +571 -0
  292. package/src/tests/slack-render-v2-query-plans.test.ts +24 -2
  293. package/src/tests/status.test.ts +11 -0
  294. package/src/tests/tasks-readable-row.test.ts +155 -0
  295. package/src/tests/tool-annotations.test.ts +2 -1
  296. package/src/tests/workflow-engine-v2.test.ts +42 -0
  297. package/src/tests/workflow-script-principal.test.ts +207 -7
  298. package/src/tools/send-task.ts +35 -47
  299. package/src/tools/slack-leave-thread.ts +104 -0
  300. package/src/tools/steer-task.ts +3 -2
  301. package/src/tools/task-dedup.ts +45 -21
  302. package/src/tools/tool-config.ts +2 -1
  303. package/src/types.ts +57 -0
  304. package/src/utils/codex-auth-failure.ts +30 -0
  305. package/src/utils/context-window.ts +4 -1
  306. package/src/utils/credentials.ts +7 -0
  307. package/src/utils/fx-models.ts +43 -0
  308. package/src/workflows/cooldown.ts +3 -2
  309. package/src/workflows/engine.ts +24 -9
  310. package/templates/schedules/dreaming-daily/config.json +1 -1
  311. package/templates/schedules/dreaming-daily/content.md +6 -14
  312. package/templates/skills/composio-google-docs/content.md +10 -0
  313. package/templates/skills/script-workflows/content.md +19 -1
  314. package/templates/skills/swarm-docs/content.md +2 -0
  315. package/templates/skills/swarm-scripts/SKILL.md +2 -0
  316. package/templates/skills/swarm-scripts/content.md +2 -0
  317. package/templates/workflows/dreaming/content.md +5 -1
  318. package/dist/acp-adapter-96dc64m2.js +0 -15
  319. package/dist/cli-dznrgaep.js +0 -30
  320. package/dist/cli-w3hcrxht.js +0 -6985
  321. package/dist/handlers-jg7v74ty.js +0 -31
  322. package/dist/index-8c1zbz59.js +0 -29
  323. package/dist/index-8k8tmfxd.js +0 -32
  324. package/dist/index-hq617xdj.js +0 -36
  325. package/dist/lead-567kw7x3.js +0 -68
  326. package/dist/worker-fjdqra6q.js +0 -68
@@ -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`,
@@ -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
  {
@@ -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
- if (!(error instanceof TaskCreationBlockedError)) {
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: error instanceof TaskCreationBlockedError ? error.reason : "error",
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
+ }
@@ -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
- const steering =
230
- files.length === 0
231
- ? await requestSlackThreadSteering({
232
- channelId,
233
- threadTs,
234
- message:
235
- nextSenderIndex === -1
236
- ? combinedText
237
- : await rewriteSlackMentions(
238
- steeringItems.map((item) => item.text).join("\n---\n"),
239
- botUserId,
240
- ),
241
- messageTimestamps: steeringItems.map((item) => item.ts),
242
- requestedByUserId,
243
- })
244
- : null;
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,