@desplega.ai/agent-swarm 2.7.5 → 2.7.7

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 (319) hide show
  1. package/dist/{acp-adapter-g3ads55s.js → acp-adapter-7b4vk3h5.js} +5 -4
  2. package/dist/{actions-zwfykqqk.js → actions-3tt153af.js} +18 -17
  3. package/dist/{amp-adapter-z3s0ssdt.js → amp-adapter-njpts0yr.js} +1 -1
  4. package/dist/{anthropic-messages-x4q6rfq5.js → anthropic-messages-xey2skwx.js} +7 -7
  5. package/dist/{app-vmp4fgep.js → app-nky4378f.js} +3 -3
  6. package/dist/{assistant-vwde653f.js → assistant-jnb669pa.js} +24 -22
  7. package/dist/{azure-openai-responses-s1w5n70z.js → azure-openai-responses-26t7j9ra.js} +5 -5
  8. package/dist/{boot-reembed-375t7tbk.js → boot-reembed-9mgvaztt.js} +1 -1
  9. package/dist/{boot-reembed-gq9rcpnj.js → boot-reembed-qs2djdej.js} +1 -1
  10. package/dist/{boot-scrub-sweep-nd938kyf.js → boot-scrub-sweep-yjpdj7s1.js} +3 -3
  11. package/dist/{boot-seeds-aykx51v3.js → boot-seeds-5cm6ambd.js} +4 -4
  12. package/dist/{boxd-k1j26g3h.js → boxd-nv507va8.js} +5 -5
  13. package/dist/{claude-adapter-8j4hm099.js → claude-adapter-fn287zmm.js} +5 -4
  14. package/dist/{claude-managed-setup-0wyynpn1.js → claude-managed-setup-wtdz2mj2.js} +1 -1
  15. package/dist/{claude-sdk-session-q6wj5hmy.js → claude-sdk-session-x91kqyhm.js} +9677 -9514
  16. package/dist/{cli-j3jvnpe8.js → cli-08bmz9z9.js} +1 -1
  17. package/dist/{cli-gjr2y2xk.js → cli-0agcv1v5.js} +4 -1
  18. package/dist/{cli-y24sc29k.js → cli-1c6tzt8g.js} +30 -8
  19. package/dist/{cli-mjzynrse.js → cli-35rsp8ye.js} +1 -1
  20. package/dist/{cli-rpm2sgv0.js → cli-3fg0bdcv.js} +19 -2
  21. package/dist/{cli-nn4d8dne.js → cli-5b456bpf.js} +2 -2
  22. package/dist/{cli-bhjt02p8.js → cli-5jnhv72d.js} +1 -1
  23. package/dist/{cli-fqzx7gwa.js → cli-5zx699j9.js} +2 -2
  24. package/dist/{cli-hf6zt5m6.js → cli-69szn7w7.js} +17 -1
  25. package/dist/{cli-vqypxzh4.js → cli-6g0hb4cp.js} +8 -8
  26. package/dist/{cli-tqgg40ek.js → cli-6pfd54vx.js} +9 -8
  27. package/dist/{cli-bsn8nhrq.js → cli-6vcp76qk.js} +3 -3
  28. package/dist/{cli-zppwg9p9.js → cli-72v799xn.js} +1 -1
  29. package/dist/cli-82j36f2m.js +45 -0
  30. package/dist/{cli-hn9hxmz9.js → cli-8305gbq0.js} +29 -16
  31. package/dist/{cli-z8eh3ex8.js → cli-8sckt74b.js} +8 -8
  32. package/dist/{cli-bnz7a7qf.js → cli-95jzxcp3.js} +1 -1
  33. package/dist/{cli-0bxqnvq9.js → cli-adeegh7d.js} +29 -7
  34. package/dist/{cli-g61qxmge.js → cli-afkqkrtw.js} +3 -3
  35. package/dist/{cli-7a42nfy0.js → cli-ayayvfg4.js} +24 -4
  36. package/dist/{cli-bqt1mptt.js → cli-az48n0nr.js} +4 -4
  37. package/dist/{cli-z6jkx23q.js → cli-bfv7vdg6.js} +9 -9
  38. package/dist/{cli-pywf3eh5.js → cli-caj33vcq.js} +5 -5
  39. package/dist/{cli-5pag6rmn.js → cli-cb2a8pna.js} +1 -1
  40. package/dist/{cli-wvaawacd.js → cli-cppkf404.js} +5236 -72
  41. package/dist/{cli-mry0qzym.js → cli-ddaanqkd.js} +123 -10
  42. package/dist/{cli-0gey5fzy.js → cli-e0kmphd6.js} +2 -2
  43. package/dist/{cli-zh3fp8v2.js → cli-e46hg0xw.js} +1 -1
  44. package/dist/{cli-jm2vfpfd.js → cli-e5bzrn01.js} +1 -1
  45. package/dist/{cli-qxnf8z2z.js → cli-excnnjg2.js} +165 -153
  46. package/dist/{cli-71ptav7x.js → cli-ferkkrg9.js} +1 -1
  47. package/dist/{cli-vpf89f34.js → cli-fmjd0t1y.js} +3 -3
  48. package/dist/{cli-rm6hqj3z.js → cli-fyt2g4wd.js} +33 -139
  49. package/dist/cli-g08zdaec.js +55 -0
  50. package/dist/{cli-ehq736cn.js → cli-g7s47cc5.js} +4 -4
  51. package/dist/{cli-82ae98pz.js → cli-gz6q4eve.js} +3 -3
  52. package/dist/{cli-82jg7fp9.js → cli-he5nxaa8.js} +1 -1
  53. package/dist/{cli-rqkxrmgw.js → cli-je518txm.js} +39 -14
  54. package/dist/{cli-8hs41zy2.js → cli-jtjtabxe.js} +1 -1
  55. package/dist/{cli-611a1294.js → cli-k0wwfqbg.js} +18 -1
  56. package/dist/{cli-nzxr556s.js → cli-k1nje3v3.js} +38 -43
  57. package/dist/{cli-3h4nhjn8.js → cli-k8196sh2.js} +9 -7
  58. package/dist/{cli-afrrppsw.js → cli-k899ehvh.js} +2 -2
  59. package/dist/{cli-0731hvbw.js → cli-m0da8j35.js} +23 -9
  60. package/dist/cli-m2z8jagn.js +12078 -0
  61. package/dist/{cli-8veytvbh.js → cli-mkk9rnzz.js} +5 -5
  62. package/dist/{cli-7wspm5gm.js → cli-n6bv12jn.js} +528 -6
  63. package/dist/{cli-spknvxbq.js → cli-nmzhp1b5.js} +14 -12
  64. package/dist/{cli-p599f1qm.js → cli-p82cnnjq.js} +1 -1
  65. package/dist/{cli-xtjhfmpc.js → cli-pc6n7fre.js} +1 -1
  66. package/dist/{cli-vbyfwh92.js → cli-pjctyea3.js} +2 -2
  67. package/dist/{cli-nmbmy64z.js → cli-pq73h5mz.js} +3 -3
  68. package/dist/{cli-aa6yxc7m.js → cli-pz4rk021.js} +1 -1
  69. package/dist/{cli-7wcyk2s5.js → cli-q3tkegby.js} +1 -1
  70. package/dist/cli-qghe0h66.js +67 -0
  71. package/dist/{cli-6s7vdcta.js → cli-qh4d4tvk.js} +4 -4
  72. package/dist/{cli-mmwm7zbf.js → cli-rtt8g4v9.js} +1 -1
  73. package/dist/{cli-kg8hchyg.js → cli-sa02jsqt.js} +1 -1
  74. package/dist/{cli-1h3wcpbw.js → cli-snpj5zbj.js} +2 -2
  75. package/dist/{cli-a7v854rf.js → cli-tgrv9cjx.js} +263 -12
  76. package/dist/{cli-a47xy8eq.js → cli-vz22fyqv.js} +186 -54
  77. package/dist/{cli-1ayk1eas.js → cli-wk5yrfwd.js} +285 -42
  78. package/dist/{cli-1338796d.js → cli-ws19768n.js} +1755 -119
  79. package/dist/{cli-70r77n04.js → cli-xae4g51r.js} +1 -1
  80. package/dist/{cli-gf5hbmdr.js → cli-yjdgea32.js} +3 -3
  81. package/dist/{cli-xbkdm710.js → cli-yndmfxb3.js} +3 -3
  82. package/dist/{cli-rhw40jgb.js → cli-yyj3j5m4.js} +2 -2
  83. package/dist/cli.js +26 -25
  84. package/dist/{cloudflare-workers-ai-system-one-bm60fjm0.js → cloudflare-workers-ai-system-one-ef2a980q.js} +2 -2
  85. package/dist/{codex-adapter-q6k3gnyg.js → codex-adapter-cf2yxe9m.js} +3 -2
  86. package/dist/{codex-hook-9efm2x1m.js → codex-hook-a0c6g6t6.js} +1 -1
  87. package/dist/{codex-session-runner-3mwmk3d9.js → codex-session-runner-6vfzdgbd.js} +3 -2
  88. package/dist/{commands-evrenjn9.js → commands-t3mfy7vn.js} +9 -3
  89. package/dist/{controller-xjp8vswh.js → controller-q7xrbaz0.js} +8 -3
  90. package/dist/{controllers-connect-pp5jnxvc.js → controllers-connect-c6nm70kg.js} +20 -19
  91. package/dist/{cursor-adapter-yf47w4t0.js → cursor-adapter-ftd81ga8.js} +1 -1
  92. package/dist/{db-zxpw6aht.js → db-dk572x25.js} +3 -3
  93. package/dist/{db-rgfsh91f.js → db-vemtpmcc.js} +14 -4
  94. package/dist/{devtools-82fzqra8.js → devtools-qxfgbsqz.js} +1 -1
  95. package/dist/{dispatcher-smasgws7.js → dispatcher-kh7ecxh9.js} +17 -16
  96. package/dist/{docker-rrjhaqt9.js → docker-c1a3gqmf.js} +89 -22
  97. package/dist/{dsh-adapter-ftyy8dnp.js → dsh-adapter-v8628r89.js} +1 -1
  98. package/dist/{e2b-w1p0bnpm.js → e2b-fr12yccx.js} +5 -5
  99. package/dist/{e2b-qcaj675y.js → e2b-x6ywzak3.js} +3 -3
  100. package/dist/{easter-egg-3d-yyts7p16.js → easter-egg-3d-nenw4x63.js} +1 -1
  101. package/dist/{embedding-space-1z0mbg95.js → embedding-space-0jepb239.js} +6 -6
  102. package/dist/{embeddings-ygeg7tm7.js → embeddings-xxt49yxg.js} +1 -1
  103. package/dist/{execute-14qhqp4d.js → execute-1mhw6e4x.js} +1 -1
  104. package/dist/{external-channel-ttgrs3sx.js → external-channel-55v12e60.js} +2 -2
  105. package/dist/{fx-adapter-gt6pncj2.js → fx-adapter-bs8wqrk5.js} +6 -5
  106. package/dist/{google-generative-ai-yvmdxyxa.js → google-generative-ai-v94hr7h0.js} +5 -5
  107. package/dist/{google-vertex-jrtdh9gs.js → google-vertex-g7vr6cf8.js} +5 -5
  108. package/dist/{grok-adapter-sm4m5k4g.js → grok-adapter-gpjqmkcq.js} +6 -5
  109. package/dist/handlers-zxmp1ta4.js +32 -0
  110. package/dist/{heartbeat-qahz4p62.js → heartbeat-k42svnr0.js} +20 -19
  111. package/dist/{hook-4fd5wq5w.js → hook-114m7hap.js} +4 -3
  112. package/dist/{hook-kj3d398m.js → hook-6nphweh0.js} +24 -9
  113. package/dist/{http-1ted0j1k.js → http-r8cc4wz7.js} +1196 -211
  114. package/dist/index-a8phxqnk.js +37 -0
  115. package/dist/{index-q4b917zs.js → index-e3s8wmxy.js} +1 -1
  116. package/dist/{index-6rq3r5zb.js → index-fpk4bcd9.js} +1 -1
  117. package/dist/{index-5rk6rfzr.js → index-fvzke5kx.js} +1 -1
  118. package/dist/index-k6h00t4c.js +30 -0
  119. package/dist/{index-7v51awbn.js → index-nh3wyqdt.js} +1027 -577
  120. package/dist/{index-yqhz88yy.js → index-pxn30mse.js} +8 -8
  121. package/dist/index-xrz7te8h.js +33 -0
  122. package/dist/{keepalive-6cf311t5.js → keepalive-x060nt9p.js} +3 -3
  123. package/dist/lead-956sd2yc.js +71 -0
  124. package/dist/{maintenance-a4bm1bmt.js → maintenance-2xxfexhf.js} +1 -1
  125. package/dist/{mistral-conversations-4pmwj1c0.js → mistral-conversations-3gjpdb4b.js} +4 -4
  126. package/dist/{oauth-refresh-sweep-3h0dfw1k.js → oauth-refresh-sweep-fckc4ar5.js} +2 -2
  127. package/dist/{onboard-myn5hraa.js → onboard-zc48j237.js} +5 -5
  128. package/dist/{openai-codex-responses-44j573nm.js → openai-codex-responses-8pbxq9h5.js} +5 -5
  129. package/dist/{openai-completions-a4gee2a8.js → openai-completions-rm00s912.js} +4 -4
  130. package/dist/{openai-decisions-w4qkrcar.js → openai-decisions-8v957r2r.js} +1 -1
  131. package/dist/{openai-responses-rbp6h5kr.js → openai-responses-agxpf96k.js} +5 -5
  132. package/dist/{opencode-adapter-p2nkb5am.js → opencode-adapter-r690h1cr.js} +4 -3
  133. package/dist/{otel-impl-a1a0ef6b.js → otel-impl-r5xbh81q.js} +4 -4
  134. package/dist/{pi-mono-adapter-rzmpb72a.js → pi-mono-adapter-c7v8sdec.js} +7 -6
  135. package/dist/{pricing-refresh-83drrnxj.js → pricing-refresh-3pmre11v.js} +3 -3
  136. package/dist/{rbac-roles-s8y6ecb5.js → rbac-roles-3vbshwsb.js} +1 -1
  137. package/dist/{rbac-roles-0fb6e7pj.js → rbac-roles-910mmkgz.js} +2 -2
  138. package/dist/{render-v2-twmbgrz4.js → render-v2-pxykkbab.js} +3 -3
  139. package/dist/{resume-zfqc36xj.js → resume-adda0bwy.js} +6 -6
  140. package/dist/{runtime-qwggdpqr.js → runtime-fnbm7stk.js} +1 -1
  141. package/dist/{session-access-sc9pn9da.js → session-access-2fvamrdc.js} +4 -4
  142. package/dist/{session-source-backfill-qh07xr8g.js → session-source-backfill-08xvv0sm.js} +2 -2
  143. package/dist/{setup-4fr0m7a1.js → setup-1aqmvmh1.js} +3 -3
  144. package/dist/{typesafe-system-one-5mavm259.js → typesafe-system-one-0bmqcxfg.js} +2 -2
  145. package/dist/{virtual-modules-fh4ha15b.js → virtual-modules-d2pjk9j1.js} +5 -5
  146. package/dist/{work-objects-0wr8zxct.js → work-objects-9s0yrm5j.js} +20 -19
  147. package/dist/{worker-follow-up-ktyyazyp.js → worker-follow-up-3q78fqmr.js} +17 -16
  148. package/dist/worker-y15hy08h.js +71 -0
  149. package/openapi.json +289 -4
  150. package/package.json +9 -8
  151. package/src/apps/definition.ts +6 -5
  152. package/src/be/db/page-theme-tokens.ts +12 -0
  153. package/src/be/db/script-runs.ts +23 -0
  154. package/src/be/db/tasks/read.ts +131 -4
  155. package/src/be/db/tasks/write.ts +19 -0
  156. package/src/be/db-client.ts +114 -11
  157. package/src/be/db.ts +49 -6
  158. package/src/be/memory/providers/sqlite-store.ts +49 -12
  159. package/src/be/migrations/260_runtime_pool_cache.sql +20 -0
  160. package/src/be/migrations/262_session_visibility_indexes.sql +18 -0
  161. package/src/be/migrations/263_task_list_filter_index.sql +11 -0
  162. package/src/be/migrations/264_slack_thread_cover_index.sql +9 -0
  163. package/src/be/migrations/266_script_runs_source_task.sql +11 -0
  164. package/src/be/migrations/269_pages_design_system.sql +23 -0
  165. package/src/be/runtime-pools/cache-cleanup.ts +109 -0
  166. package/src/be/runtime-pools/db.ts +65 -6
  167. package/src/be/runtime-pools/remote-controller.ts +5 -0
  168. package/src/be/runtime-pools/supervisor.ts +33 -2
  169. package/src/be/scripts/typecheck.ts +22 -0
  170. package/src/be/seed/schedules-seeder.ts +0 -10
  171. package/src/be/seed-scripts/catalog/catalog-report.inline.ts +2 -0
  172. package/src/be/seed-scripts/catalog/catalog-report.ts +2 -0
  173. package/src/be/seed-scripts/catalog/memory-eval.ts +2 -0
  174. package/src/be/seed-scripts/catalog/swarm-update-check.ts +150 -0
  175. package/src/be/seed-scripts/catalog/task-failure-audit.ts +5 -1
  176. package/src/be/seed-scripts/index.ts +9 -0
  177. package/src/be/seed-skills/bundled-files.generated.json +3 -3
  178. package/src/be/swarm-config-guard.ts +32 -0
  179. package/src/be/task-auto-title-listener.ts +127 -0
  180. package/src/commands/controller.ts +4 -0
  181. package/src/commands/entrypoint-api-readiness.ts +74 -0
  182. package/src/commands/onboard/agent-fs-version.ts +1 -1
  183. package/src/commands/profile-sync.ts +151 -16
  184. package/src/commands/runner.ts +15 -7
  185. package/src/hooks/hook.ts +20 -9
  186. package/src/http/index.ts +4 -1
  187. package/src/http/list-total-cache.ts +14 -7
  188. package/src/http/memory.ts +38 -9
  189. package/src/http/pages-public.ts +93 -129
  190. package/src/http/pages.ts +94 -21
  191. package/src/http/script-runs.ts +174 -5
  192. package/src/http/status.ts +10 -2
  193. package/src/http/tasks.ts +4 -2
  194. package/src/pages/browser-sdk.ts +48 -21
  195. package/src/pages/color-literals.ts +115 -0
  196. package/src/pages/page-head.ts +766 -0
  197. package/src/pages/version.ts +3 -0
  198. package/src/providers/amp-adapter.ts +1 -1
  199. package/src/providers/codex-adapter.ts +1 -0
  200. package/src/providers/codex-app-server.ts +27 -1
  201. package/src/providers/fx-adapter.ts +1 -1
  202. package/src/providers/pi-mono-extension.ts +30 -15
  203. package/src/runtime-providers/controller-protocol.ts +14 -1
  204. package/src/runtime-providers/controller.ts +5 -0
  205. package/src/runtime-providers/docker.ts +108 -23
  206. package/src/runtime-providers/types.ts +16 -0
  207. package/src/script-workflows/workflow-ctx.ts +8 -0
  208. package/src/scripts-runtime/types/stdlib.d.ts +22 -0
  209. package/src/scripts-runtime/types/swarm-sdk.d.ts +22 -0
  210. package/src/server-user.ts +1 -0
  211. package/src/slack/commands.ts +5 -1
  212. package/src/slack/handlers.ts +37 -2
  213. package/src/slack/left-threads.ts +27 -4
  214. package/src/tasks/follow-up-gate.ts +318 -0
  215. package/src/tasks/follow-up-redelegation-guard.ts +155 -0
  216. package/src/tasks/worker-follow-up.ts +21 -1
  217. package/src/tests/claude-artifacts-read-script.test.ts +209 -0
  218. package/src/tests/codex-app-server.test.ts +59 -0
  219. package/src/tests/codex-gateway-route.test.ts +50 -1
  220. package/src/tests/create-page-tool.test.ts +501 -0
  221. package/src/tests/db-client-busy-retry.test.ts +81 -0
  222. package/src/tests/db-client.test.ts +21 -0
  223. package/src/tests/db-task-read-contract.test.ts +9 -2
  224. package/src/tests/db-task-write-contract.test.ts +7 -1
  225. package/src/tests/db-tasks-read.test.ts +21 -0
  226. package/src/tests/entrypoint-api-readiness.test.ts +416 -210
  227. package/src/tests/explicit-model-validation.test.ts +14 -8
  228. package/src/tests/fixtures/pages/head-v1.html +56 -0
  229. package/src/tests/follow-up-gate-shadow.test.ts +513 -0
  230. package/src/tests/follow-up-redelivery-guard.test.ts +432 -202
  231. package/src/tests/identity-field-budget.test.ts +1 -1
  232. package/src/tests/internal-ai/credentials.test.ts +62 -0
  233. package/src/tests/internal-ai/task-title.test.ts +283 -0
  234. package/src/tests/list-total-cache.test.ts +27 -2
  235. package/src/tests/memory-fts-populate.test.ts +11 -9
  236. package/src/tests/memory-retrieval-purge.test.ts +80 -0
  237. package/src/tests/merge-gate-detect-changes.test.ts +145 -0
  238. package/src/tests/migration-260-runtime-pool-cache.test.ts +118 -0
  239. package/src/tests/page-proxy.test.ts +1 -1
  240. package/src/tests/page-session-dev-cookie.test.ts +188 -0
  241. package/src/tests/pages-frame-ancestors.test.ts +28 -1
  242. package/src/tests/pages-head-v2.test.ts +534 -0
  243. package/src/tests/pages-http.test.ts +330 -0
  244. package/src/tests/pages-public-html.test.ts +43 -0
  245. package/src/tests/pages-storage.test.ts +182 -0
  246. package/src/tests/pages-versioning.test.ts +72 -0
  247. package/src/tests/profile-sync-stale-writeback.test.ts +334 -0
  248. package/src/tests/profile-sync.test.ts +11 -15
  249. package/src/tests/query-audit-plans.test.ts +27 -0
  250. package/src/tests/runtime-cache-cleanup.test.ts +367 -0
  251. package/src/tests/runtime-e2b-pools.test.ts +41 -0
  252. package/src/tests/runtime-pools-db.test.ts +98 -0
  253. package/src/tests/runtime-pools-http.test.ts +29 -0
  254. package/src/tests/runtime-providers-docker.test.ts +253 -0
  255. package/src/tests/runtime-remote-controller.test.ts +66 -2
  256. package/src/tests/runtime-supervisor.test.ts +66 -2
  257. package/src/tests/scheduled-task-required-params-migration.test.ts +3 -1
  258. package/src/tests/script-connections.test.ts +13 -0
  259. package/src/tests/script-runs-http.test.ts +684 -2
  260. package/src/tests/seed-registry.test.ts +28 -6
  261. package/src/tests/seed-schedules.test.ts +61 -8
  262. package/src/tests/seed-scripts.test.ts +168 -2
  263. package/src/tests/slack-leave-thread.test.ts +145 -5
  264. package/src/tests/slack-render-v2-query-plans.test.ts +24 -2
  265. package/src/tests/status.test.ts +11 -0
  266. package/src/tests/swarm-diff.test.ts +121 -2
  267. package/src/tests/task-auto-title-config-guard.test.ts +37 -0
  268. package/src/tests/task-auto-title-listener.test.ts +165 -0
  269. package/src/tests/tasks-readable-row.test.ts +155 -0
  270. package/src/tests/theme-import.test.ts +758 -0
  271. package/src/tests/themes-page-theme.test.ts +155 -0
  272. package/src/tests/themes-presets.test.ts +172 -0
  273. package/src/tests/workflow-script-principal.test.ts +2 -1
  274. package/src/themes/classics.ts +949 -0
  275. package/src/themes/import.ts +420 -0
  276. package/src/themes/page-fields.ts +40 -0
  277. package/src/themes/page-theme.ts +102 -0
  278. package/src/themes/presets.ts +561 -0
  279. package/src/themes/tokens.ts +218 -0
  280. package/src/tools/app-upsert.ts +1 -1
  281. package/src/tools/create-page.ts +118 -12
  282. package/src/tools/send-task.ts +35 -47
  283. package/src/tools/slack-leave-thread.ts +1 -1
  284. package/src/tools/task-dedup.ts +45 -21
  285. package/src/tools/utils.ts +33 -0
  286. package/src/types.ts +83 -3
  287. package/src/utils/constants.ts +19 -0
  288. package/src/utils/follow-up-gate-schedule-ids.ts +13 -0
  289. package/src/utils/internal-ai/credentials.ts +146 -93
  290. package/src/utils/internal-ai/index.ts +10 -0
  291. package/src/utils/internal-ai/models.ts +18 -0
  292. package/src/utils/internal-ai/task-title.ts +282 -0
  293. package/src/utils/page-session.ts +37 -0
  294. package/templates/schedules/daily-status-report/content.md +88 -2
  295. package/templates/schedules/daily-swarm-update-check/content.md +16 -40
  296. package/templates/skills/apps/SKILL.md +1 -1
  297. package/templates/skills/apps/content.md +1 -1
  298. package/templates/skills/composio-google-docs/content.md +10 -0
  299. package/templates/skills/heartbeat-runbook/content.md +1 -1
  300. package/templates/skills/pages/config.json +1 -1
  301. package/templates/skills/pages/content.md +539 -86
  302. package/templates/skills/pages/files/examples/annotated-pr.html +21 -12
  303. package/templates/skills/pages/files/examples/multiplayer-board.html +41 -41
  304. package/templates/skills/pages/files/examples/report-page.html +125 -124
  305. package/templates/skills/script-workflows/content.md +19 -1
  306. package/templates/skills/swarm-docs/content.md +1 -0
  307. package/templates/skills/swarm-scripts/SKILL.md +2 -0
  308. package/templates/skills/swarm-scripts/content.md +2 -0
  309. package/dist/cli-2pnswgh8.js +0 -189
  310. package/dist/cli-dznrgaep.js +0 -30
  311. package/dist/cli-w3hcrxht.js +0 -6985
  312. package/dist/handlers-vwmba1m2.js +0 -31
  313. package/dist/index-5r71rf98.js +0 -32
  314. package/dist/index-anmwxs87.js +0 -29
  315. package/dist/index-spp0ra5c.js +0 -36
  316. package/dist/lead-fjy418cw.js +0 -69
  317. package/dist/worker-0c5tyzqe.js +0 -69
  318. package/templates/schedules/daily-workflow-health-audit/config.json +0 -19
  319. package/templates/schedules/daily-workflow-health-audit/content.md +0 -191
@@ -4,15 +4,23 @@ import {
4
4
  cancelTask,
5
5
  closeDb,
6
6
  completeTask,
7
+ countAgentDispatchesInThread,
7
8
  createAgent,
8
9
  createTaskExtended,
9
10
  failTask,
10
- findCompletedTaskInThread,
11
+ findCompletedTasksInThread,
11
12
  findRecentCancelledTaskInThread,
12
13
  getDbClient,
13
- getTaskById,
14
14
  initDb,
15
15
  } from "../be/db";
16
+ import {
17
+ FOLLOW_UP_DISPATCH_CAP,
18
+ FOLLOW_UP_DISPATCH_WINDOW_MINUTES,
19
+ FOLLOW_UP_SAME_WORK_PAGE_SIZE,
20
+ } from "../tasks/follow-up-redelegation-guard";
21
+ import { sendTaskHandler, sendTaskInputSchema } from "../tools/send-task";
22
+ import { DEDUP_SCAN_LIMIT, findDuplicateTask } from "../tools/task-dedup";
23
+ import { ownerCtx } from "../tools/task-tool-ctx";
16
24
 
17
25
  const TEST_DB_PATH = "./test-follow-up-redelivery-guard.sqlite";
18
26
 
@@ -31,7 +39,7 @@ afterAll(() => {
31
39
  }
32
40
  });
33
41
 
34
- describe("findCompletedTaskInThread", () => {
42
+ describe("findCompletedTasksInThread", () => {
35
43
  test("finds completed tasks in a thread within the time window", async () => {
36
44
  const agent = await createAgent({
37
45
  name: "dedup-worker-1",
@@ -50,13 +58,12 @@ describe("findCompletedTaskInThread", () => {
50
58
  await completeTask(task.id, "done");
51
59
 
52
60
  // Should find the completed task within a 2880-minute (48h) window
53
- const result = await findCompletedTaskInThread("C_DEDUP_1", "1000.0001", 2880);
54
- expect(result).not.toBeNull();
55
- expect(result!.id).toBe(task.id);
56
- expect(result!.status).toBe("completed");
61
+ const result = await findCompletedTasksInThread("C_DEDUP_1", "1000.0001", 2880);
62
+ expect(result.map((t) => t.id)).toEqual([task.id]);
63
+ expect(result[0]!.status).toBe("completed");
57
64
  });
58
65
 
59
- test("returns null when no completed tasks exist in the thread", async () => {
66
+ test("returns nothing when no completed tasks exist in the thread", async () => {
60
67
  const agent = await createAgent({
61
68
  name: "dedup-worker-2",
62
69
  isLead: false,
@@ -71,11 +78,11 @@ describe("findCompletedTaskInThread", () => {
71
78
  slackThreadTs: "2000.0001",
72
79
  });
73
80
 
74
- const result = await findCompletedTaskInThread("C_DEDUP_2", "2000.0001", 2880);
75
- expect(result).toBeNull();
81
+ const result = await findCompletedTasksInThread("C_DEDUP_2", "2000.0001", 2880);
82
+ expect(result).toEqual([]);
76
83
  });
77
84
 
78
- test("returns null outside the time window", async () => {
85
+ test("returns nothing outside the time window", async () => {
79
86
  const agent = await createAgent({
80
87
  name: "dedup-worker-3",
81
88
  isLead: false,
@@ -99,11 +106,11 @@ describe("findCompletedTaskInThread", () => {
99
106
  ]);
100
107
 
101
108
  // Should not find with a 48 hour window
102
- const result = await findCompletedTaskInThread("C_DEDUP_3", "3000.0001", 2880);
103
- expect(result).toBeNull();
109
+ const result = await findCompletedTasksInThread("C_DEDUP_3", "3000.0001", 2880);
110
+ expect(result).toEqual([]);
104
111
  });
105
112
 
106
- test("returns null for a different thread", async () => {
113
+ test("returns nothing for a different thread", async () => {
107
114
  const agent = await createAgent({
108
115
  name: "dedup-worker-4",
109
116
  isLead: false,
@@ -120,118 +127,73 @@ describe("findCompletedTaskInThread", () => {
120
127
  await completeTask(task.id, "done");
121
128
 
122
129
  // Search in a different thread — should not find
123
- const result = await findCompletedTaskInThread("C_DEDUP_4", "4000.9999", 2880);
124
- expect(result).toBeNull();
130
+ const result = await findCompletedTasksInThread("C_DEDUP_4", "4000.9999", 2880);
131
+ expect(result).toEqual([]);
125
132
  });
126
- });
127
-
128
- describe("follow-up re-delegation guard logic", () => {
129
- let leadAgent: Awaited<ReturnType<typeof createAgent>>;
130
- let workerAgent: Awaited<ReturnType<typeof createAgent>>;
131
133
 
132
- beforeAll(async () => {
133
- leadAgent = await createAgent({
134
- name: "guard-lead",
135
- isLead: true,
136
- status: "idle",
137
- capabilities: [],
138
- });
139
- workerAgent = await createAgent({
140
- name: "guard-worker",
134
+ test("caps the rows it returns at the limit, newest first", async () => {
135
+ const agent = await createAgent({
136
+ name: "dedup-worker-5",
141
137
  isLead: false,
142
138
  status: "idle",
143
139
  capabilities: [],
144
- maxTasks: 5,
145
140
  });
141
+ const ids: string[] = [];
142
+ for (let i = 0; i < 3; i++) {
143
+ const task = await createTaskExtended(`capped completion ${i}`, {
144
+ agentId: agent.id,
145
+ slackChannelId: "C_DEDUP_5",
146
+ slackThreadTs: "5000.0001",
147
+ });
148
+ await completeTask(task.id, "done");
149
+ await getDbClient().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
150
+ new Date(Date.now() - (3 - i) * 60 * 1000).toISOString(),
151
+ task.id,
152
+ ]);
153
+ ids.push(task.id);
154
+ }
155
+
156
+ const result = await findCompletedTasksInThread("C_DEDUP_5", "5000.0001", 2880, 2);
157
+ expect(result.map((t) => t.id)).toEqual([ids[2], ids[1]]);
146
158
  });
147
159
 
148
- test("blocks re-delegation when source task is a follow-up and thread has completed work", async () => {
149
- // Step 1: Create and complete a worker task in a Slack thread
150
- const workerTask = await createTaskExtended("implement feature X", {
151
- agentId: workerAgent.id,
152
- slackChannelId: "C_GUARD_1",
153
- slackThreadTs: "5000.0001",
154
- });
155
- await completeTask(workerTask.id, "Feature X implemented");
156
-
157
- // Step 2: Create a follow-up task (as store-progress would)
158
- const followUpTask = await createTaskExtended("Worker task completed — review needed.", {
159
- agentId: leadAgent.id,
160
- source: "system",
161
- taskType: "follow-up",
162
- parentTaskId: workerTask.id,
163
- slackChannelId: "C_GUARD_1",
164
- slackThreadTs: "5000.0001",
165
- });
166
-
167
- // Step 3: Simulate the guard logic from send-task.ts
168
- // The lead's sourceTaskId would be the follow-up task
169
- const sourceTask = await getTaskById(followUpTask.id);
170
- expect(sourceTask).not.toBeNull();
171
- expect(sourceTask!.taskType).toBe("follow-up");
172
- expect(sourceTask!.slackChannelId).toBe("C_GUARD_1");
173
- expect(sourceTask!.slackThreadTs).toBe("5000.0001");
174
-
175
- // The guard should find the completed worker task
176
- const recentCompleted = await findCompletedTaskInThread(
177
- sourceTask!.slackChannelId!,
178
- sourceTask!.slackThreadTs!,
179
- 2880,
180
- );
181
- expect(recentCompleted).not.toBeNull();
182
- expect(recentCompleted!.id).toBe(workerTask.id);
183
-
184
- // → Guard would block: re-delegation should be prevented
185
- });
186
-
187
- test("allows delegation when source task is NOT a follow-up (normal behavior)", async () => {
188
- // Create a normal Slack task (not a follow-up)
189
- const slackTask = await createTaskExtended("user asked a question", {
190
- agentId: leadAgent.id,
191
- source: "slack",
192
- taskType: "inbox",
193
- slackChannelId: "C_GUARD_2",
194
- slackThreadTs: "6000.0001",
195
- });
196
-
197
- // Even if there are completed tasks in the thread, guard shouldn't trigger
198
- // because the source task is not a "follow-up"
199
- const sourceTask = await getTaskById(slackTask.id);
200
- expect(sourceTask).not.toBeNull();
201
- expect(sourceTask!.taskType).not.toBe("follow-up");
202
-
203
- // Guard condition: sourceTask?.taskType === "follow-up" → false
204
- // → Guard does NOT block: delegation proceeds normally
205
- const shouldBlock =
206
- sourceTask?.taskType === "follow-up" && sourceTask.slackThreadTs && sourceTask.slackChannelId;
207
- expect(shouldBlock).toBeFalsy();
208
- });
209
-
210
- test("allows delegation when source task is a follow-up but thread has NO completed work", async () => {
211
- // Create a follow-up task in a thread with no completed work
212
- const followUpTask = await createTaskExtended("Worker task failed — action needed.", {
213
- agentId: leadAgent.id,
214
- source: "system",
215
- taskType: "follow-up",
216
- slackChannelId: "C_GUARD_3",
217
- slackThreadTs: "7000.0001",
160
+ test("the before cursor returns the next page, including rows tied on lastUpdatedAt", async () => {
161
+ const agent = await createAgent({
162
+ name: "dedup-worker-6",
163
+ isLead: false,
164
+ status: "idle",
165
+ capabilities: [],
218
166
  });
219
-
220
- const sourceTask = await getTaskById(followUpTask.id);
221
- expect(sourceTask).not.toBeNull();
222
- expect(sourceTask!.taskType).toBe("follow-up");
223
-
224
- // No completed tasks in this thread
225
- const recentCompleted = await findCompletedTaskInThread(
226
- sourceTask!.slackChannelId!,
227
- sourceTask!.slackThreadTs!,
228
- 2880,
229
- );
230
- expect(recentCompleted).toBeNull();
231
-
232
- // → Guard does NOT block: first-time delegation is fine
167
+ const at = new Date(Date.now() - 5 * 60 * 1000).toISOString();
168
+ for (let i = 0; i < 5; i++) {
169
+ const task = await createTaskExtended(`paged completion ${i}`, {
170
+ agentId: agent.id,
171
+ slackChannelId: "C_DEDUP_6",
172
+ slackThreadTs: "6000.0001",
173
+ });
174
+ await completeTask(task.id, "done");
175
+ await getDbClient().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
176
+ at,
177
+ task.id,
178
+ ]);
179
+ }
180
+
181
+ const seen: string[] = [];
182
+ let before: { lastUpdatedAt: string; id: string } | undefined;
183
+ // Bounded so an ignored cursor fails the test instead of looping forever.
184
+ for (let i = 0; i < 10; i++) {
185
+ const page = await findCompletedTasksInThread("C_DEDUP_6", "6000.0001", 2880, 2, before);
186
+ seen.push(...page.map((t) => t.id));
187
+ const last = page.at(-1);
188
+ if (!last || page.length < 2) break;
189
+ before = { lastUpdatedAt: last.lastUpdatedAt, id: last.id };
190
+ }
191
+ expect(seen).toHaveLength(5);
192
+ expect(new Set(seen).size).toBe(5);
233
193
  });
194
+ });
234
195
 
196
+ describe("findRecentCancelledTaskInThread", () => {
235
197
  test("findRecentCancelledTaskInThread finds tasks with status='cancelled'", async () => {
236
198
  const agent = await createAgent({
237
199
  name: "cancel-thread-worker-1",
@@ -308,125 +270,393 @@ describe("follow-up re-delegation guard logic", () => {
308
270
  const result = await findRecentCancelledTaskInThread("C_CANCEL_4", "9000.0004", 2880);
309
271
  expect(result).toBeNull();
310
272
  });
273
+ });
274
+
275
+ describe("send-task follow-up re-delegation guard", () => {
276
+ let leadAgent: Awaited<ReturnType<typeof createAgent>>;
277
+ let workerAgent: Awaited<ReturnType<typeof createAgent>>;
278
+
279
+ beforeAll(async () => {
280
+ leadAgent = await createAgent({
281
+ name: "guard-lead",
282
+ isLead: true,
283
+ status: "idle",
284
+ capabilities: [],
285
+ });
286
+ workerAgent = await createAgent({
287
+ name: "guard-worker",
288
+ isLead: false,
289
+ status: "idle",
290
+ capabilities: [],
291
+ maxTasks: 100,
292
+ });
293
+ });
311
294
 
312
- test("guard bypasses re-delegation block when cancellation is more recent than completion", async () => {
313
- const channel = "C_BYPASS_1";
314
- const thread = "10000.0001";
295
+ async function backdate(taskId: string, minutesAgo: number): Promise<void> {
296
+ const at = new Date(Date.now() - minutesAgo * 60 * 1000).toISOString();
297
+ await getDbClient().run(
298
+ "UPDATE agent_tasks SET createdAt = ?, lastUpdatedAt = ? WHERE id = ?",
299
+ [at, at, taskId],
300
+ );
301
+ }
315
302
 
316
- // Step 1: An old completed task in the thread
317
- const completedTask = await createTaskExtended("first attempt — completed", {
303
+ /** A worker task in the thread that completed `minutesAgo` minutes ago. */
304
+ async function completedWork(channel: string, thread: string, text: string, minutesAgo = 30) {
305
+ const task = await createTaskExtended(text, {
318
306
  agentId: workerAgent.id,
319
307
  slackChannelId: channel,
320
308
  slackThreadTs: thread,
321
309
  });
322
- await completeTask(completedTask.id, "first attempt done");
323
-
324
- // Backdate to 30 minutes ago so the cancellation is more recent.
325
- const thirtyMinAgo = new Date(Date.now() - 30 * 60 * 1000).toISOString();
326
- await getDbClient().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
327
- thirtyMinAgo,
328
- completedTask.id,
329
- ]);
310
+ await completeTask(task.id, "done");
311
+ await backdate(task.id, minutesAgo);
312
+ return task;
313
+ }
330
314
 
331
- // Step 2: A more-recent cancellation in the same thread
332
- const cancelledTask = await createTaskExtended("second attempt — cancelled mid-work", {
333
- agentId: workerAgent.id,
315
+ /** The Lead session store-progress opens after a worker finishes. */
316
+ async function leadSession(channel: string, thread: string, taskType = "follow-up") {
317
+ return createTaskExtended("Worker task completed — review needed.", {
318
+ agentId: leadAgent.id,
319
+ source: "system",
320
+ taskType,
334
321
  slackChannelId: channel,
335
322
  slackThreadTs: thread,
336
323
  });
337
- await cancelTask(cancelledTask.id, "cancelled");
324
+ }
338
325
 
339
- // Guard checks:
340
- const recentCompleted = await findCompletedTaskInThread(channel, thread, 2880);
341
- const recentCancelled = await findRecentCancelledTaskInThread(channel, thread, 2880);
342
- expect(recentCompleted).not.toBeNull();
343
- expect(recentCancelled).not.toBeNull();
326
+ function dispatch(sourceTaskId: string, task: string, extra: Record<string, unknown> = {}) {
327
+ return sendTaskHandler(
328
+ ownerCtx({ agentId: leadAgent.id, sourceTaskId }),
329
+ sendTaskInputSchema.parse({
330
+ task,
331
+ agentId: workerAgent.id,
332
+ routingReason: "skill",
333
+ routingNote: "worker owns this repo",
334
+ ...extra,
335
+ }),
336
+ );
337
+ }
344
338
 
345
- // The bypass condition: cancellation is more recent than completion.
346
- const cancelledMoreRecent =
347
- recentCancelled &&
348
- new Date(recentCancelled.lastUpdatedAt).getTime() >
349
- new Date(recentCompleted!.lastUpdatedAt).getTime();
350
- expect(cancelledMoreRecent).toBe(true);
339
+ test("a distinct next-step dispatch from a follow-up passes", async () => {
340
+ await completedWork("C_NEXT", "1.1", "implement the billing export endpoint in the api server");
341
+ const followUp = await leadSession("C_NEXT", "1.1");
342
+ await completeTask(followUp.id, "dispatched step 1");
343
+ const session = await leadSession("C_NEXT", "1.1");
351
344
 
352
- // → Guard does NOT block: re-delegation is allowed.
345
+ const result = await dispatch(
346
+ session.id,
347
+ "review pull request 42 and post findings as comments",
348
+ );
349
+ expect(result.ok).toBe(true);
350
+ expect(result.data?.task?.slackThreadTs).toBe("1.1");
353
351
  });
354
352
 
355
- test("guard still blocks when completion is more recent than any cancellation", async () => {
356
- const channel = "C_BYPASS_2";
357
- const thread = "11000.0001";
353
+ test("re-dispatching completed work from a follow-up is blocked by the same-work rule", async () => {
354
+ const done = await completedWork("C_SAME", "2.1", "fix the flaky retry test in the scheduler");
355
+ const session = await leadSession("C_SAME", "2.1");
358
356
 
359
- // Step 1: A cancelled task (older)
360
- const cancelledTask = await createTaskExtended("attempt 1 — cancelled", {
361
- agentId: workerAgent.id,
362
- slackChannelId: channel,
363
- slackThreadTs: thread,
357
+ const result = await dispatch(session.id, "fix the flaky retry test in the scheduler");
358
+ expect(result.ok).toBe(false);
359
+ expect(result.message).toContain("same-work rule");
360
+ expect(result.message).toContain(done.id.slice(0, 8));
361
+ expect(result.message).toContain("allowRedelegation: true");
362
+ });
363
+
364
+ test("allowDuplicate does not bypass the same-work rule", async () => {
365
+ await completedWork("C_DUP", "3.1", "rotate the staging database credentials now");
366
+ const session = await leadSession("C_DUP", "3.1");
367
+
368
+ const result = await dispatch(session.id, "rotate the staging database credentials now", {
369
+ allowDuplicate: true,
364
370
  });
365
- await cancelTask(cancelledTask.id, "cancelled");
371
+ expect(result.ok).toBe(false);
372
+ expect(result.message).toContain("same-work rule");
373
+ });
366
374
 
367
- // Backdate the cancellation to 30 minutes ago
368
- const thirtyMinAgo = new Date(Date.now() - 30 * 60 * 1000).toISOString();
369
- await getDbClient().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
370
- thirtyMinAgo,
371
- cancelledTask.id,
372
- ]);
375
+ test("matching completed work stays blocked behind 50 newer unrelated completions", async () => {
376
+ const done = await completedWork(
377
+ "C_DEEP",
378
+ "2.5",
379
+ "migrate the audit log table to the new schema",
380
+ 120,
381
+ );
382
+ for (let i = 0; i < 50; i++) {
383
+ await completedWork("C_DEEP", "2.5", `unrelated chore ${i} touching widget ${i}`, 60 - i);
384
+ }
385
+ const session = await leadSession("C_DEEP", "2.5");
386
+
387
+ const result = await dispatch(session.id, "migrate the audit log table to the new schema");
388
+ expect(result.ok).toBe(false);
389
+ expect(result.message).toContain("same-work rule");
390
+ expect(result.message).toContain(done.id.slice(0, 8));
391
+ });
392
+
393
+ test("matching completed work stays blocked behind more than a page of newer unrelated completions", async () => {
394
+ const done = await completedWork(
395
+ "C_DEEPER",
396
+ "2.6",
397
+ "backfill the invoice totals for the march billing run",
398
+ 600,
399
+ );
400
+ for (let i = 0; i < FOLLOW_UP_SAME_WORK_PAGE_SIZE + 50; i++) {
401
+ await completedWork("C_DEEPER", "2.6", `unrelated chore ${i} touching widget ${i}`, 300 - i);
402
+ }
403
+ const session = await leadSession("C_DEEPER", "2.6");
404
+
405
+ const result = await dispatch(
406
+ session.id,
407
+ "backfill the invoice totals for the march billing run",
408
+ );
409
+ expect(result.ok).toBe(false);
410
+ expect(result.message).toContain("same-work rule");
411
+ expect(result.message).toContain(done.id.slice(0, 8));
412
+ });
373
413
 
374
- // Step 2: A more-recent completion (the retry succeeded)
375
- const completedTask = await createTaskExtended("attempt 2 — completed", {
414
+ test("the loop cap blocks a distinct dispatch once the thread hit the cap", async () => {
415
+ for (let i = 0; i < FOLLOW_UP_DISPATCH_CAP; i++) {
416
+ await createTaskExtended(`loop step number ${i} with unique words ${i * 7}`, {
417
+ agentId: workerAgent.id,
418
+ creatorAgentId: leadAgent.id,
419
+ source: "mcp",
420
+ slackChannelId: "C_LOOP",
421
+ slackThreadTs: "4.1",
422
+ });
423
+ }
424
+ const session = await leadSession("C_LOOP", "4.1");
425
+
426
+ const result = await dispatch(
427
+ session.id,
428
+ "draft the quarterly changelog summary for customers",
429
+ );
430
+ expect(result.ok).toBe(false);
431
+ expect(result.message).toContain("loop-cap rule");
432
+ expect(result.message).toContain(`cap ${FOLLOW_UP_DISPATCH_CAP}`);
433
+ expect(result.message).toContain("allowRedelegation: true");
434
+ });
435
+
436
+ /** A durable run's `ctx.step.agentTask` step tasks, created by the Lead in the thread. */
437
+ async function runSteps(channel: string, thread: string, steps: number) {
438
+ const runId = crypto.randomUUID();
439
+ for (let i = 0; i < steps; i++) {
440
+ await createTaskExtended(`deck slide ${i} for run ${runId.slice(0, 8)} words ${i * 11}`, {
441
+ agentId: workerAgent.id,
442
+ creatorAgentId: leadAgent.id,
443
+ source: "mcp",
444
+ taskType: "script-run-step",
445
+ contextKey: `script-run:${runId}:slide-${i}`,
446
+ slackChannelId: channel,
447
+ slackThreadTs: thread,
448
+ });
449
+ }
450
+ return runId;
451
+ }
452
+
453
+ test("a 29-step durable run plus one send-task counts as two dispatches", async () => {
454
+ await runSteps("C_LOOP_RUN", "4.3", 29);
455
+ await createTaskExtended("one plain send-task dispatch into the thread", {
376
456
  agentId: workerAgent.id,
377
- slackChannelId: channel,
378
- slackThreadTs: thread,
457
+ creatorAgentId: leadAgent.id,
458
+ source: "mcp",
459
+ slackChannelId: "C_LOOP_RUN",
460
+ slackThreadTs: "4.3",
379
461
  });
380
- await completeTask(completedTask.id, "retry succeeded");
381
-
382
- // Guard:
383
- const recentCompleted = await findCompletedTaskInThread(channel, thread, 2880);
384
- const recentCancelled = await findRecentCancelledTaskInThread(channel, thread, 2880);
385
- expect(recentCompleted).not.toBeNull();
386
- expect(recentCancelled).not.toBeNull();
462
+ expect(
463
+ await countAgentDispatchesInThread(
464
+ "C_LOOP_RUN",
465
+ "4.3",
466
+ leadAgent.id,
467
+ FOLLOW_UP_DISPATCH_WINDOW_MINUTES,
468
+ ),
469
+ ).toBe(2);
470
+ const session = await leadSession("C_LOOP_RUN", "4.3");
471
+
472
+ const result = await dispatch(session.id, "summarize the deck review notes for the team");
473
+ expect(result.ok).toBe(true);
474
+ });
387
475
 
388
- const cancelledMoreRecent =
389
- recentCancelled &&
390
- new Date(recentCancelled.lastUpdatedAt).getTime() >
391
- new Date(recentCompleted!.lastUpdatedAt).getTime();
392
- expect(cancelledMoreRecent).toBe(false);
476
+ test("the loop cap counts each durable run and each send-task once", async () => {
477
+ for (let i = 0; i < FOLLOW_UP_DISPATCH_CAP / 2; i++) {
478
+ await runSteps("C_LOOP_MIX", "4.4", 3);
479
+ await createTaskExtended(`mixed loop send-task ${i} unique words ${i * 17}`, {
480
+ agentId: workerAgent.id,
481
+ creatorAgentId: leadAgent.id,
482
+ source: "mcp",
483
+ slackChannelId: "C_LOOP_MIX",
484
+ slackThreadTs: "4.4",
485
+ });
486
+ }
487
+ const session = await leadSession("C_LOOP_MIX", "4.4");
488
+
489
+ const result = await dispatch(session.id, "write the release announcement for the blog");
490
+ expect(result.ok).toBe(false);
491
+ expect(result.message).toContain("loop-cap rule");
492
+ expect(result.message).toContain(`dispatched ${FOLLOW_UP_DISPATCH_CAP} times`);
493
+ });
393
494
 
394
- // → Guard BLOCKS as before: the work was already redone successfully.
495
+ test("dispatches older than the loop-cap window do not count", async () => {
496
+ for (let i = 0; i < FOLLOW_UP_DISPATCH_CAP; i++) {
497
+ const old = await createTaskExtended(`old loop step ${i} distinct text ${i * 13}`, {
498
+ agentId: workerAgent.id,
499
+ creatorAgentId: leadAgent.id,
500
+ source: "mcp",
501
+ slackChannelId: "C_LOOP_OLD",
502
+ slackThreadTs: "4.2",
503
+ });
504
+ await backdate(old.id, 7 * 60);
505
+ }
506
+ const session = await leadSession("C_LOOP_OLD", "4.2");
507
+
508
+ const result = await dispatch(session.id, "audit the onboarding emails for broken links");
509
+ expect(result.ok).toBe(true);
395
510
  });
396
511
 
397
- test("allows delegation when source task is a follow-up but completed work is outside time window", async () => {
398
- // Create and complete a worker task, then backdate it
399
- const oldWorkerTask = await createTaskExtended("old task", {
512
+ test("a failed task more recent than the completion lets the same work through", async () => {
513
+ await completedWork("C_FAIL", "5.1", "migrate the reports page to the new table component");
514
+ const failed = await createTaskExtended("migrate the reports page, second attempt", {
400
515
  agentId: workerAgent.id,
401
- slackChannelId: "C_GUARD_4",
402
- slackThreadTs: "8000.0001",
516
+ slackChannelId: "C_FAIL",
517
+ slackThreadTs: "5.1",
403
518
  });
404
- await completeTask(oldWorkerTask.id, "done long ago");
519
+ await failTask(failed.id, "TypeError: cannot read property of undefined");
520
+ const session = await leadSession("C_FAIL", "5.1");
405
521
 
406
- // Backdate to 49 hours ago (beyond the 48h window)
407
- const fortyNineHoursAgo = new Date(Date.now() - 49 * 60 * 60 * 1000).toISOString();
408
- await getDbClient().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
409
- fortyNineHoursAgo,
410
- oldWorkerTask.id,
411
- ]);
522
+ const result = await dispatch(
523
+ session.id,
524
+ "migrate the reports page to the new table component",
525
+ );
526
+ expect(result.ok).toBe(true);
527
+ });
412
528
 
413
- // Create a follow-up in the same thread
414
- const followUpTask = await createTaskExtended("Worker task completed — review needed.", {
415
- agentId: leadAgent.id,
416
- source: "system",
417
- taskType: "follow-up",
418
- slackChannelId: "C_GUARD_4",
419
- slackThreadTs: "8000.0001",
529
+ test("a failure older than the completion does not bypass the same-work rule", async () => {
530
+ const failed = await createTaskExtended("compress the landing hero images, first try", {
531
+ agentId: workerAgent.id,
532
+ slackChannelId: "C_FAIL_OLD",
533
+ slackThreadTs: "5.2",
420
534
  });
535
+ await failTask(failed.id, "network error");
536
+ await backdate(failed.id, 60);
537
+ await completedWork("C_FAIL_OLD", "5.2", "compress the landing hero images to webp", 30);
538
+ const session = await leadSession("C_FAIL_OLD", "5.2");
539
+
540
+ const result = await dispatch(session.id, "compress the landing hero images to webp");
541
+ expect(result.ok).toBe(false);
542
+ expect(result.message).toContain("same-work rule");
543
+ });
544
+
545
+ test("a cancelled task more recent than the completion still lets the same work through", async () => {
546
+ await completedWork("C_CANCEL", "6.1", "translate the pricing page into spanish copy");
547
+ const cancelled = await createTaskExtended("translate the pricing page, rerun", {
548
+ agentId: workerAgent.id,
549
+ slackChannelId: "C_CANCEL",
550
+ slackThreadTs: "6.1",
551
+ });
552
+ await cancelTask(cancelled.id, "cancelled");
553
+ const session = await leadSession("C_CANCEL", "6.1");
554
+
555
+ const result = await dispatch(session.id, "translate the pricing page into spanish copy");
556
+ expect(result.ok).toBe(true);
557
+ });
558
+
559
+ test("allowRedelegation overrides both rules", async () => {
560
+ await completedWork("C_OVERRIDE", "7.1", "regenerate the openapi spec after the route change");
561
+ const session = await leadSession("C_OVERRIDE", "7.1");
421
562
 
422
- const sourceTask = await getTaskById(followUpTask.id);
423
- const recentCompleted = await findCompletedTaskInThread(
424
- sourceTask!.slackChannelId!,
425
- sourceTask!.slackThreadTs!,
426
- 2880,
563
+ const blocked = await dispatch(
564
+ session.id,
565
+ "regenerate the openapi spec after the route change",
427
566
  );
428
- expect(recentCompleted).toBeNull();
567
+ expect(blocked.ok).toBe(false);
568
+
569
+ const result = await dispatch(
570
+ session.id,
571
+ "regenerate the openapi spec after the route change",
572
+ { allowRedelegation: true },
573
+ );
574
+ expect(result.ok).toBe(true);
575
+
576
+ for (let i = 0; i < FOLLOW_UP_DISPATCH_CAP; i++) {
577
+ await createTaskExtended(`override loop filler ${i} words ${i * 11}`, {
578
+ agentId: workerAgent.id,
579
+ creatorAgentId: leadAgent.id,
580
+ source: "mcp",
581
+ slackChannelId: "C_OVERRIDE",
582
+ slackThreadTs: "7.1",
583
+ });
584
+ }
585
+ const capped = await dispatch(session.id, "write release notes for the export feature", {
586
+ allowRedelegation: true,
587
+ });
588
+ expect(capped.ok).toBe(true);
589
+ });
590
+
591
+ test("a resume source task is exempt from the guard", async () => {
592
+ await completedWork("C_RESUME", "8.1", "finish the interrupted search index rebuild");
593
+ const resume = await leadSession("C_RESUME", "8.1", "resume");
594
+
595
+ const result = await dispatch(resume.id, "finish the interrupted search index rebuild");
596
+ expect(result.ok).toBe(true);
597
+ });
598
+
599
+ test("a non-follow-up source task is not guarded", async () => {
600
+ await completedWork("C_INBOX", "9.1", "summarize the open incidents for this week");
601
+ const inbox = await leadSession("C_INBOX", "9.1", "inbox");
602
+
603
+ const result = await dispatch(inbox.id, "summarize the open incidents for this week");
604
+ expect(result.ok).toBe(true);
605
+ });
606
+ });
607
+
608
+ describe("findDuplicateTask", () => {
609
+ test("finds an older duplicate behind 50 newer unrelated tasks from the same creator", async () => {
610
+ const creator = await createAgent({
611
+ name: "dedup-deep-creator",
612
+ isLead: true,
613
+ status: "idle",
614
+ capabilities: [],
615
+ });
616
+ const original = await createTaskExtended("backfill the invoice totals for october", {
617
+ creatorAgentId: creator.id,
618
+ });
619
+ await getDbClient().run("UPDATE agent_tasks SET createdAt = ? WHERE id = ?", [
620
+ new Date(Date.now() - 5 * 60 * 1000).toISOString(),
621
+ original.id,
622
+ ]);
623
+ for (let i = 0; i < 50; i++) {
624
+ await createTaskExtended(`unrelated chore ${i} touching widget ${i}`, {
625
+ creatorAgentId: creator.id,
626
+ });
627
+ }
628
+
629
+ const match = await findDuplicateTask({
630
+ taskDescription: "backfill the invoice totals for october",
631
+ creatorAgentId: creator.id,
632
+ });
633
+ expect(match?.task.id).toBe(original.id);
634
+ });
429
635
 
430
- // → Guard does NOT block: completed work is too old
636
+ test("scans at most DEDUP_SCAN_LIMIT rows, so a large backlog keeps one dispatch bounded", async () => {
637
+ const creator = await createAgent({
638
+ name: "dedup-backlog-creator",
639
+ isLead: true,
640
+ status: "idle",
641
+ capabilities: [],
642
+ });
643
+ const original = await createTaskExtended("reconcile the ledger exports for november", {
644
+ creatorAgentId: creator.id,
645
+ });
646
+ await getDbClient().run("UPDATE agent_tasks SET createdAt = ? WHERE id = ?", [
647
+ new Date(Date.now() - 5 * 60 * 1000).toISOString(),
648
+ original.id,
649
+ ]);
650
+ for (let i = 0; i < DEDUP_SCAN_LIMIT; i++) {
651
+ await createTaskExtended(`backlog chore ${i} touching gadget ${i}`, {
652
+ creatorAgentId: creator.id,
653
+ });
654
+ }
655
+
656
+ const match = await findDuplicateTask({
657
+ taskDescription: "reconcile the ledger exports for november",
658
+ creatorAgentId: creator.id,
659
+ });
660
+ expect(match).toBeNull();
431
661
  });
432
662
  });