@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
@@ -0,0 +1,155 @@
1
+ import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { closeDb, getAllTasks, getDb, getDbClient, initDb } from "../be/db";
4
+ import { readableSessionCondition } from "../be/db/tasks/read";
5
+
6
+ const TEST_DB_PATH = "./test-tasks-readable-row.sqlite";
7
+ const AT = "2026-10-01T00:00:00.000Z";
8
+
9
+ async function removeDbFiles(): Promise<void> {
10
+ for (const suffix of ["", "-wal", "-shm"])
11
+ await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => undefined);
12
+ }
13
+
14
+ async function task(
15
+ id: string,
16
+ opts: { parent?: string; owner?: string; scheduleId?: string } = {},
17
+ ): Promise<void> {
18
+ await getDbClient().run(
19
+ `INSERT INTO agent_tasks (id, task, status, source, parentTaskId, requestedByUserId, scheduleId,
20
+ createdAt, lastUpdatedAt)
21
+ VALUES (?, ?, 'completed', 'api', ?, ?, ?, ?, ?)`,
22
+ [id, `task ${id}`, opts.parent ?? null, opts.owner ?? null, opts.scheduleId ?? null, AT, AT],
23
+ );
24
+ }
25
+
26
+ describe("readableSessionRowCondition", () => {
27
+ beforeAll(async () => {
28
+ await removeDbFiles();
29
+ initDb(TEST_DB_PATH);
30
+ for (const id of ["u1", "u2", "u3"]) {
31
+ await getDbClient().run(
32
+ "INSERT INTO users (id, name, role, status, createdAt, lastUpdatedAt) VALUES (?, ?, 'member', 'active', ?, ?)",
33
+ [id, id, AT, AT],
34
+ );
35
+ }
36
+ // u1's session, three deep.
37
+ await task("a", { owner: "u1" });
38
+ await task("a1", { parent: "a", owner: "u1" });
39
+ await task("a2", { parent: "a1" });
40
+ // u2's session, with a child that names u1.
41
+ await task("b", { owner: "u2" });
42
+ await task("b1", { parent: "b", owner: "u1" });
43
+ // u2's schedule run: everyone reads it.
44
+ await task("c", { owner: "u2", scheduleId: "schedule-1" });
45
+ await task("c1", { parent: "c" });
46
+ // u2's session shared with u1, and one shared with everyone.
47
+ await task("d", { owner: "u2" });
48
+ await task("d1", { parent: "d" });
49
+ await task("e", { owner: "u2" });
50
+ await getDbClient().run(
51
+ `INSERT INTO session_shares (id, rootTaskId, userId, access, createdAt)
52
+ VALUES ('share-d', 'd', 'u1', 'read', ?), ('share-e', 'e', NULL, 'read', ?)`,
53
+ [AT, AT],
54
+ );
55
+ // A revoked share hides again.
56
+ await task("f", { owner: "u2" });
57
+ await getDbClient().run(
58
+ `INSERT INTO session_shares (id, rootTaskId, userId, access, createdAt, revokedAt)
59
+ VALUES ('share-f', 'f', 'u1', 'read', ?, ?)`,
60
+ [AT, AT],
61
+ );
62
+ // A session nobody owns.
63
+ await task("g");
64
+ await task("g1", { parent: "g", owner: "u2" });
65
+ // Orphan subtrees: the top's parent is gone, so the top is the root.
66
+ await task("h", { owner: "u2" });
67
+ await task("h1", { parent: "h" });
68
+ await task("h2", { parent: "h1", owner: "u1" });
69
+ await task("i", { owner: "u1" });
70
+ await task("i1", { parent: "i", owner: "u2" });
71
+ await task("i2", { parent: "i1" });
72
+ // Chains deeper than the walk's hop limit fall back to the set form.
73
+ for (const [root, owner] of [
74
+ ["deep-u1", "u1"],
75
+ ["deep-u2", "u2"],
76
+ ] as const) {
77
+ await task(root, { owner });
78
+ let parent: string = root;
79
+ for (let i = 1; i <= 70; i++) {
80
+ await task(`${root}-${i}`, { parent });
81
+ parent = `${root}-${i}`;
82
+ }
83
+ }
84
+ getDb().run("PRAGMA foreign_keys = OFF");
85
+ getDb().run("DELETE FROM agent_tasks WHERE id IN ('h', 'i')");
86
+ getDb().run("PRAGMA foreign_keys = ON");
87
+ });
88
+
89
+ afterAll(async () => {
90
+ closeDb();
91
+ await removeDbFiles();
92
+ });
93
+
94
+ for (const userId of ["u1", "u2", "u3", null]) {
95
+ test(`the page shows the rows the set form shows, for ${userId ?? "a guest"}`, async () => {
96
+ const set = readableSessionCondition(userId);
97
+ const expected = (
98
+ await getDbClient().query<{ id: string }>(
99
+ `SELECT id FROM agent_tasks WHERE ${set.sql} ORDER BY id`,
100
+ set.params,
101
+ )
102
+ ).map((row) => row.id);
103
+ const page = await getAllTasks({ readableBy: { userId }, limit: 1000 });
104
+ expect(page.map((t) => t.id).sort()).toEqual(expected);
105
+ });
106
+ }
107
+
108
+ test("u1 sees exactly the expected sessions", async () => {
109
+ const page = await getAllTasks({ readableBy: { userId: "u1" }, limit: 1000 });
110
+ const deep = Array.from({ length: 70 }, (_, i) => `deep-u1-${i + 1}`);
111
+ expect(page.map((t) => t.id).sort()).toEqual(
112
+ [
113
+ "a",
114
+ "a1",
115
+ "a2",
116
+ "c",
117
+ "c1",
118
+ "d",
119
+ "d1",
120
+ "e",
121
+ "g",
122
+ "g1",
123
+ "h1",
124
+ "h2",
125
+ "deep-u1",
126
+ ...deep,
127
+ ].sort(),
128
+ );
129
+ });
130
+
131
+ test("the page walks up from each row it reads", async () => {
132
+ const query = spyOn(getDbClient(), "query");
133
+ let sql = "";
134
+ let params: unknown[] = [];
135
+ try {
136
+ await getAllTasks({ status: "completed", readableBy: { userId: "u1" } });
137
+ [sql, params = []] = query.mock.calls.find(([s]) => s.includes("WITH page AS"))! as [
138
+ string,
139
+ unknown[]?,
140
+ ];
141
+ } finally {
142
+ query.mockRestore();
143
+ }
144
+ const plan = getDb()
145
+ .query<{ detail: string }, any[]>(`EXPLAIN QUERY PLAN ${sql}`)
146
+ .all(...(params as any[]))
147
+ .map((row) => row.detail)
148
+ .join("\n");
149
+ // The walk is a correlated lookup per row. The set form appears only as
150
+ // the fallback branch, which SQLite builds when a walk finds no root.
151
+ expect(plan).toContain("CORRELATED SCALAR SUBQUERY");
152
+ expect(plan).toContain("SEARCH u USING INDEX sqlite_autoindex_agent_tasks_1 (id=?)");
153
+ expect(plan).toContain("SEARCH p USING INDEX sqlite_autoindex_agent_tasks_1 (id=?)");
154
+ });
155
+ });
@@ -181,6 +181,7 @@ describe("Tool Annotations & Classification", () => {
181
181
  "slack-list-channels",
182
182
  "slack-delete",
183
183
  "slack-update",
184
+ "slack-leave-thread",
184
185
  ];
185
186
 
186
187
  for (const name of slackTools) {
@@ -379,7 +380,7 @@ describe("Tool Annotations & Classification", () => {
379
380
  // the six Chat SDK tools (chat-* + register/unregister-chat-channel),
380
381
  // and the seven app row tools plus app-action-run.
381
382
  expect(count).toBeGreaterThanOrEqual(45);
382
- expect(count).toBeLessThanOrEqual(169);
383
+ expect(count).toBeLessThanOrEqual(170);
383
384
  });
384
385
 
385
386
  test("core tools are fewer than deferred tools", () => {
@@ -945,6 +945,48 @@ describe("Workflow Engine v2 (Phase 3)", () => {
945
945
  expect(run2!.status).toBe("skipped");
946
946
  expect(run2!.error).toBe("cooldown");
947
947
  });
948
+
949
+ test("simultaneous triggers start one run; the other records a cooldown skip", async () => {
950
+ const registry = createTestRegistry();
951
+ const def: WorkflowDefinition = {
952
+ nodes: [{ id: "a", type: "echo", config: { message: "hi" } }],
953
+ };
954
+ const workflow = await makeWorkflow(def, { cooldown: { hours: 20, from: "start" } });
955
+
956
+ // Both pass the pre-transaction check before either run row exists.
957
+ const runIds = await Promise.all([
958
+ startWorkflowExecution(workflow, {}, registry),
959
+ startWorkflowExecution(workflow, {}, registry),
960
+ ]);
961
+ const statuses = await Promise.all(
962
+ runIds.map(async (id) => (await getWorkflowRun(id))!.status),
963
+ );
964
+ expect(statuses.sort()).toEqual(["completed", "skipped"]);
965
+ });
966
+
967
+ test('from: "start" ignores a long successful run that finished inside the window', async () => {
968
+ const { createWorkflowRun, updateWorkflowRun } = await import("../be/db");
969
+ const def: WorkflowDefinition = {
970
+ nodes: [{ id: "a", type: "echo", config: {} }],
971
+ };
972
+ const workflow = await makeWorkflow(def);
973
+
974
+ // Started 24h ago, finished 1h ago: 24h since its start, 1h since its finish.
975
+ const runId = crypto.randomUUID();
976
+ await createWorkflowRun({ id: runId, workflowId: workflow.id });
977
+ await getDbClient().run("UPDATE workflow_runs SET startedAt = ? WHERE id = ?", [
978
+ new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
979
+ runId,
980
+ ]);
981
+ await updateWorkflowRun(runId, {
982
+ status: "completed",
983
+ finishedAt: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
984
+ });
985
+
986
+ expect(await shouldSkipCooldown(workflow.id, { hours: 20, from: "start" })).toBe(false);
987
+ // Control: the default also counts from the successful finish.
988
+ expect(await shouldSkipCooldown(workflow.id, { hours: 20 })).toBe(true);
989
+ });
948
990
  });
949
991
 
950
992
  // ─── Input Resolution ─────────────────────────────────────
@@ -13,14 +13,19 @@ import {
13
13
  createAgent,
14
14
  createScheduledTask,
15
15
  createWorkflow,
16
+ createWorkflowRun,
16
17
  getDbClient,
18
+ getScheduledTaskByName,
19
+ getWorkflowRun,
17
20
  initDb,
18
21
  listWorkflows,
22
+ updateWorkflow,
19
23
  } from "../be/db";
20
24
  import { upsertScriptByName } from "../be/scripts/db";
21
25
  import { setScriptEmbeddingProviderForTests } from "../be/scripts/embeddings";
22
26
  import * as runCredential from "../be/scripts/run-credential";
23
27
  import { runSeeder } from "../be/seed";
28
+ import { createSchedulesSeeder } from "../be/seed/schedules-seeder";
24
29
  import { createWorkflowsSeeder } from "../be/seed/workflows-seeder";
25
30
  import { getPathSegments, parseQueryParams } from "../http/utils";
26
31
  import { handleWorkflows } from "../http/workflows";
@@ -34,6 +39,7 @@ import {
34
39
  workflowEventBus,
35
40
  } from "../workflows";
36
41
  import { handleEventTrigger } from "../workflows/triggers";
42
+ import { snapshotAndUpdateWorkflow } from "../workflows/version";
37
43
  import { listenOnFreePort } from "./test-net";
38
44
 
39
45
  // A creator-less workflow (the seeded `dreaming`) runs its `swarm-script` nodes as
@@ -160,9 +166,31 @@ afterAll(async () => {
160
166
  await removeDbFiles();
161
167
  });
162
168
 
163
- beforeEach(() => {
164
- // Dreaming has no cooldown, so earlier runs (parked on an agent-task) do not
165
- // block the next trigger.
169
+ /** Put the seeded dreaming row back after a test edits it. */
170
+ async function restoreDreaming(): Promise<void> {
171
+ await updateWorkflow(dreaming.id, {
172
+ description: dreaming.description,
173
+ definition: dreaming.definition,
174
+ input: dreaming.input,
175
+ cooldown: dreaming.cooldown,
176
+ enabled: dreaming.enabled,
177
+ });
178
+ }
179
+
180
+ /** Move every dreaming run start out of its 20-hour cooldown window. */
181
+ async function ageDreamingRuns(): Promise<void> {
182
+ await getDbClient().run(
183
+ `UPDATE workflow_runs SET startedAt = '2000-01-01T00:00:00.000Z',
184
+ finishedAt = CASE WHEN finishedAt IS NULL THEN NULL ELSE '2000-01-01T00:00:00.000Z' END
185
+ WHERE workflowId = ?`,
186
+ [dreaming.id],
187
+ );
188
+ }
189
+
190
+ beforeEach(async () => {
191
+ // Dreaming has a 20-hour cooldown. Age earlier runs (parked on an agent-task)
192
+ // so they do not block the next trigger.
193
+ await ageDreamingRuns();
166
194
  calls = [];
167
195
  scriptResult = QUIET_GATHER;
168
196
  });
@@ -241,6 +269,179 @@ describe("swarm-script principal for a creator-less workflow", () => {
241
269
  expect(gatherPrincipals()).toEqual([leadId]);
242
270
  });
243
271
 
272
+ test("a creator-less schedule runs as the lead, resolved at fire time", async () => {
273
+ const schedule = await createScheduledTask({
274
+ name: `principal-seeded-${crypto.randomUUID().slice(0, 8)}`,
275
+ cronExpression: "0 0 1 1 *",
276
+ targetType: "workflow",
277
+ workflowId: dreaming.id,
278
+ targetAgentId: workerId,
279
+ });
280
+ expect(schedule.createdByAgentId).toBeUndefined();
281
+ await dispatchScheduleTarget(schedule);
282
+ expect(gatherPrincipals()).toEqual([leadId]);
283
+ });
284
+
285
+ test("a creator-less schedule fails its fire when the swarm has no lead", async () => {
286
+ const schedule = await createScheduledTask({
287
+ name: `principal-leadless-${crypto.randomUUID().slice(0, 8)}`,
288
+ cronExpression: "0 0 1 1 *",
289
+ targetType: "workflow",
290
+ workflowId: dreaming.id,
291
+ });
292
+ await getDbClient().run("UPDATE agents SET isLead = 0 WHERE id = ?", [leadId]);
293
+ try {
294
+ await expect(dispatchScheduleTarget(schedule)).rejects.toThrow(
295
+ "has no creator and the swarm has no lead",
296
+ );
297
+ } finally {
298
+ await getDbClient().run("UPDATE agents SET isLead = 1 WHERE id = ?", [leadId]);
299
+ }
300
+ expect(calls).toEqual([]);
301
+ const runs = await getDbClient().query<{ id: string }>(
302
+ "SELECT id FROM workflow_runs WHERE workflowId = ? AND startedAt > '2001-01-01'",
303
+ [dreaming.id],
304
+ );
305
+ expect(runs).toEqual([]);
306
+ });
307
+
308
+ test("a creator-less schedule keeps the lead after the documented input setup and enable", async () => {
309
+ // The guide's setup: update-workflow with every input key, then turn it on.
310
+ // Both snapshot the seeded state first, as the tool and REST paths do.
311
+ await snapshotAndUpdateWorkflow(
312
+ dreaming.id,
313
+ { input: { ...dreaming.input, leadAgentId: leadId, slackChannel: "C123" } },
314
+ { changedByAgentId: workerId },
315
+ );
316
+ await snapshotAndUpdateWorkflow(dreaming.id, { enabled: false });
317
+ await snapshotAndUpdateWorkflow(dreaming.id, {
318
+ enabled: true,
319
+ description: "tuned by the operator",
320
+ cooldown: { hours: 22, from: "start" },
321
+ });
322
+ const schedule = await createScheduledTask({
323
+ name: `principal-tuned-${crypto.randomUUID().slice(0, 8)}`,
324
+ cronExpression: "0 0 1 1 *",
325
+ targetType: "workflow",
326
+ workflowId: dreaming.id,
327
+ });
328
+ try {
329
+ await dispatchScheduleTarget(schedule);
330
+ expect(gatherPrincipals()).toEqual([leadId]);
331
+ expect(calls[0]?.args.leadAgentId).toBe(leadId);
332
+ } finally {
333
+ await restoreDreaming();
334
+ }
335
+ });
336
+
337
+ test("a creator-less schedule refuses the lead once a worker edits the seeded nodes", async () => {
338
+ const schedule = await createScheduledTask({
339
+ name: `principal-edited-${crypto.randomUUID().slice(0, 8)}`,
340
+ cronExpression: "0 0 1 1 *",
341
+ targetType: "workflow",
342
+ workflowId: dreaming.id,
343
+ });
344
+ const [gather, ...rest] = dreaming.definition.nodes;
345
+ await snapshotAndUpdateWorkflow(
346
+ dreaming.id,
347
+ {
348
+ input: { ...dreaming.input, leadAgentId: leadId },
349
+ definition: {
350
+ ...dreaming.definition,
351
+ nodes: [{ ...gather!, config: { ...gather!.config, scriptName: "evil" } }, ...rest],
352
+ },
353
+ },
354
+ { changedByAgentId: workerId },
355
+ );
356
+ try {
357
+ await expect(dispatchScheduleTarget(schedule)).rejects.toThrow(
358
+ 'the nodes or params of seeded workflow "dreaming" were edited',
359
+ );
360
+ } finally {
361
+ await restoreDreaming();
362
+ }
363
+ expect(calls).toEqual([]);
364
+ });
365
+
366
+ test("a creator-less schedule refuses the lead for an edit with no seeded snapshot", async () => {
367
+ const schedule = await createScheduledTask({
368
+ name: `principal-unsnapshotted-${crypto.randomUUID().slice(0, 8)}`,
369
+ cronExpression: "0 0 1 1 *",
370
+ targetType: "workflow",
371
+ workflowId: dreaming.id,
372
+ });
373
+ // Delete the history so no snapshot proves the seeded state.
374
+ await getDbClient().run("DELETE FROM workflow_versions WHERE workflowId = ?", [dreaming.id]);
375
+ await updateWorkflow(dreaming.id, { description: "edited without a snapshot" });
376
+ try {
377
+ await expect(dispatchScheduleTarget(schedule)).rejects.toThrow("were edited");
378
+ } finally {
379
+ await restoreDreaming();
380
+ }
381
+ expect(calls).toEqual([]);
382
+ });
383
+
384
+ test("a long successful night does not make the next nightly fire skip", async () => {
385
+ const schedule = await createScheduledTask({
386
+ name: `principal-long-${crypto.randomUUID().slice(0, 8)}`,
387
+ cronExpression: "0 0 1 1 *",
388
+ targetType: "workflow",
389
+ workflowId: dreaming.id,
390
+ });
391
+ // Last night started 24h ago and finished 1h ago.
392
+ const runId = crypto.randomUUID();
393
+ await createWorkflowRun({ id: runId, workflowId: dreaming.id });
394
+ await getDbClient().run(
395
+ "UPDATE workflow_runs SET status = 'completed', startedAt = ?, finishedAt = ? WHERE id = ?",
396
+ [
397
+ new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
398
+ new Date(Date.now() - 60 * 60 * 1000).toISOString(),
399
+ runId,
400
+ ],
401
+ );
402
+ const fired = await dispatchScheduleTarget(schedule);
403
+ const run = await getWorkflowRun(fired.workflowRunIds![0]!);
404
+ expect(run?.status).not.toBe("skipped");
405
+ expect(gatherPrincipals()).toEqual([leadId]);
406
+ });
407
+
408
+ test("control: a creator-less workflow the seeder never wrote keeps running agentless", async () => {
409
+ const copy = await createWorkflow({
410
+ name: `principal-unseeded-${crypto.randomUUID()}`,
411
+ definition: dreaming.definition,
412
+ });
413
+ const schedule = await createScheduledTask({
414
+ name: `principal-unseeded-${crypto.randomUUID().slice(0, 8)}`,
415
+ cronExpression: "0 0 1 1 *",
416
+ targetType: "workflow",
417
+ workflowId: copy.id,
418
+ });
419
+ await dispatchScheduleTarget(schedule);
420
+ expect(gatherPrincipals()).toEqual(["workflow"]);
421
+ });
422
+
423
+ test("end to end: the seeded dreaming-daily schedule fires dreaming as the lead, once a day", async () => {
424
+ const seeded = await runSeeder(createSchedulesSeeder(), { quiet: true });
425
+ expect(seeded.failed).toEqual([]);
426
+ const schedule = await getScheduledTaskByName("dreaming-daily");
427
+ expect(schedule).toMatchObject({ targetType: "workflow", workflowId: dreaming.id });
428
+ expect(schedule?.taskTemplate).toBeUndefined();
429
+ expect(schedule?.createdByAgentId).toBeUndefined();
430
+
431
+ const first = await dispatchScheduleTarget(schedule!);
432
+ expect(first.task).toBeUndefined();
433
+ expect(first.workflowRunIds).toHaveLength(1);
434
+ expect(gatherPrincipals()).toEqual([leadId]);
435
+ const run = await getWorkflowRun(first.workflowRunIds![0]!);
436
+ expect(run?.triggerData).toMatchObject({ scheduleName: "dreaming-daily" });
437
+
438
+ // The cooldown replaces the old task prompt's 20-hour double-run guard.
439
+ const second = await dispatchScheduleTarget(schedule!);
440
+ const skipped = await getWorkflowRun(second.workflowRunIds![0]!);
441
+ expect(skipped).toMatchObject({ status: "skipped", error: "cooldown" });
442
+ expect(gatherPrincipals()).toEqual([leadId]);
443
+ });
444
+
244
445
  test("control: a workflow with a creator still runs as its creator", async () => {
245
446
  const owned = await createWorkflow({
246
447
  name: `principal-owned-${crypto.randomUUID()}`,
@@ -251,10 +452,9 @@ describe("swarm-script principal for a creator-less workflow", () => {
251
452
  expect(gatherPrincipals()).toEqual([leadId]);
252
453
  });
253
454
 
254
- test("control: the seeded schedule's fire (a worker triggers with no data) runs the *-lead nodes on the lead", async () => {
255
- // dreaming-daily makes a pool task; the worker that claims it calls
256
- // trigger-workflow with no triggerData. Scripts run as that worker, while
257
- // synthesize and apply-lead target gather's leadAgentId.
455
+ test("control: a worker's manual trigger with no data runs the *-lead nodes on the lead", async () => {
456
+ // A worker that calls trigger-workflow with no triggerData. Scripts run as
457
+ // that worker, while synthesize and apply-lead target gather's leadAgentId.
258
458
  scriptResult = {
259
459
  ok: true,
260
460
  agents: [{ id: workerId, name: "principal-worker" }],
@@ -5,9 +5,7 @@ import { resolveTaskAuditUserId } from "@/be/audit-user";
5
5
  import {
6
6
  createTaskExtended,
7
7
  extensionAgentAssignmentError,
8
- findCompletedTaskInThread,
9
8
  findExistingLinearTrackerContextWork,
10
- findRecentCancelledTaskInThread,
11
9
  getActiveTaskCount,
12
10
  getAgentById,
13
11
  getDbClient,
@@ -23,6 +21,10 @@ import { repointTrackerSyncBySwarmId } from "@/be/db-queries/tracker";
23
21
  import { explicitModelErrorForAgent } from "@/be/model-validation";
24
22
  import { applyPreTaskCreate } from "@/extensions/apply-task-create";
25
23
  import { can } from "@/rbac";
24
+ import {
25
+ evaluateFollowUpRedelegation,
26
+ followUpThreadForSourceTask,
27
+ } from "@/tasks/follow-up-redelegation-guard";
26
28
  import { checkSlackRoutingCoherence } from "@/tasks/slack-routing";
27
29
  import { findDuplicateTask } from "@/tools/task-dedup";
28
30
  import { ownerCtx, type ToolCtx } from "@/tools/task-tool-ctx";
@@ -163,7 +165,13 @@ export const sendTaskInputSchema = z
163
165
  .boolean()
164
166
  .default(false)
165
167
  .describe(
166
- "If true, skip duplicate detection and create the task even if a similar one exists.",
168
+ "If true, skip duplicate detection and create the task even if a similar one exists. Does not bypass the follow-up re-delegation guard; see allowRedelegation.",
169
+ ),
170
+ allowRedelegation: z
171
+ .boolean()
172
+ .default(false)
173
+ .describe(
174
+ "Only matters when you call send-task from a follow-up task in a Slack thread. Such a dispatch is blocked when it repeats a task already completed in that thread (same-work rule, unless a task there was cancelled or failed after that completion), or when you already dispatched 10 tasks into the thread in the last 6 hours (loop-cap rule). Set true to dispatch anyway; the override is logged. Use it only for deliberate re-runs or long chains, never to retry a block you have not read.",
167
175
  ),
168
176
  slackChannelId: z
169
177
  .string()
@@ -292,6 +300,7 @@ export async function sendTaskHandler(
292
300
  effort,
293
301
  profile,
294
302
  allowDuplicate,
303
+ allowRedelegation,
295
304
  slackChannelId,
296
305
  slackThreadTs,
297
306
  slackUserId,
@@ -316,6 +325,7 @@ export async function sendTaskHandler(
316
325
  const requestedLeadOnly = ctx.kind === "user" ? false : leadOnly;
317
326
  const requestedCapabilities = ctx.kind === "user" ? undefined : requiredCapabilities;
318
327
  const requestedAllowDuplicate = ctx.kind === "user" ? false : allowDuplicate;
328
+ const requestedAllowRedelegation = ctx.kind === "user" ? false : allowRedelegation;
319
329
  const requestedOverrideSlackContext = ctx.kind === "user" ? false : overrideSlackContext;
320
330
 
321
331
  // Defense in depth for direct TypeScript callers that bypass MCP schema parsing.
@@ -572,7 +582,9 @@ export async function sendTaskHandler(
572
582
  // Every read in this handler releases the FIFO lock, so two concurrent
573
583
  // send-task calls with the same creator and text otherwise both pass the
574
584
  // guards and both create a task (no UNIQUE index arbitrates them).
575
- const evaluateDedupGuards = async (): Promise<{
585
+ const evaluateDedupGuards = async (
586
+ logOverride: boolean,
587
+ ): Promise<{
576
588
  ok: boolean;
577
589
  message: string;
578
590
  task?: AgentTask;
@@ -602,59 +614,35 @@ export async function sendTaskHandler(
602
614
  }
603
615
  }
604
616
 
605
- // Guard: prevent re-delegation from follow-up tasks
606
- // When the source task is a "follow-up" (worker completed/failed notification),
607
- // check if there are completed tasks in the same Slack thread recently.
608
- // This prevents the cycle: worker completes → follow-up → Lead re-delegates → repeat.
609
- //
610
- // Exception: if a MORE RECENT task in the same thread was cancelled (exit 130,
611
- // status='cancelled', or status='failed' with failureReason containing
612
- // "cancelled"), bypass the guard. A cancellation means the work was
613
- // interrupted — re-dispatch is the correct response, not a deduped no-op.
614
- // Without this bypass, a cancelled worker permanently jams the thread
615
- // against re-delegation when an earlier completed sibling exists.
617
+ // Guard: prevent re-delegation from follow-up tasks (same-work and
618
+ // loop-cap rules, see src/tasks/follow-up-redelegation-guard.ts).
619
+ // allowRedelegation bypasses both and is logged.
616
620
  //
617
621
  // NOTE: `taskType === "resume"` (created by createResumeFollowUp on
618
622
  // supersede) is intentionally NOT in this guard — a resume IS the legitimate
619
623
  // re-dispatch and bypassing the check is correct. Do not add "resume" here.
620
- if (sourceTaskId) {
621
- const sourceTask = await getTaskById(sourceTaskId);
622
- if (
623
- sourceTask?.taskType === "follow-up" &&
624
- sourceTask.slackThreadTs &&
625
- sourceTask.slackChannelId
626
- ) {
627
- const recentCompleted = await findCompletedTaskInThread(
628
- sourceTask.slackChannelId,
629
- sourceTask.slackThreadTs,
630
- 2880, // 48 hours in minutes
624
+ const followUpThread = creatorAgentId ? await followUpThreadForSourceTask(sourceTaskId) : null;
625
+ if (followUpThread && creatorAgentId) {
626
+ const blocked = await evaluateFollowUpRedelegation({
627
+ ...followUpThread,
628
+ creatorAgentId,
629
+ taskDescription,
630
+ targetAgentId: taskOptions.agentId ?? taskOptions.offeredTo,
631
+ });
632
+ if (blocked && !requestedAllowRedelegation) {
633
+ return { ok: false, message: blocked.message };
634
+ }
635
+ if (blocked && logOverride) {
636
+ console.log(
637
+ `[send-task] follow-up re-delegation override: rule=${blocked.rule} creatorAgentId=${creatorAgentId} sourceTaskId=${sourceTaskId} thread=${followUpThread.channelId}/${followUpThread.threadTs}`,
631
638
  );
632
- if (recentCompleted) {
633
- const recentCancelled = await findRecentCancelledTaskInThread(
634
- sourceTask.slackChannelId,
635
- sourceTask.slackThreadTs,
636
- 2880,
637
- );
638
- const cancelledMoreRecent =
639
- recentCancelled &&
640
- new Date(recentCancelled.lastUpdatedAt).getTime() >
641
- new Date(recentCompleted.lastUpdatedAt).getTime();
642
- if (!cancelledMoreRecent) {
643
- return {
644
- ok: false,
645
- message: `Blocked: re-delegation from follow-up task in a thread that already has completed work (task ${recentCompleted.id.slice(0, 8)}). The original request was already handled.`,
646
- };
647
- }
648
- // else: fall through — the cancellation is more recent than the
649
- // completion, so re-delegation is legitimate.
650
- }
651
639
  }
652
640
  }
653
641
 
654
642
  return null;
655
643
  };
656
644
 
657
- const guard = await evaluateDedupGuards();
645
+ const guard = await evaluateDedupGuards(true);
658
646
  if (guard) {
659
647
  const guardData = {
660
648
  yourAgentId: creatorAgentId,
@@ -669,7 +657,7 @@ export async function sendTaskHandler(
669
657
  // Authoritative re-check: the reads above are separated from the INSERT by
670
658
  // the whole handler, so only a guard inside this transaction can see a
671
659
  // concurrent send-task's committed task.
672
- const raced = await evaluateDedupGuards();
660
+ const raced = await evaluateDedupGuards(false);
673
661
  if (raced) return { success: raced.ok, message: raced.message, task: raced.task };
674
662
 
675
663
  // This transaction already checked the tracker key, excluding the caller's