@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
@@ -3,8 +3,11 @@
3
3
  *
4
4
  * A list total is a COUNT over the same filters as the page, and on the large
5
5
  * tables behind the sessions and task lists it costs as much as the page
6
- * itself. Page 1 (offset 0) always counts, so a write (a new session, a
7
- * triage move) shows at once where the user looks first. Later pages reuse
6
+ * itself. By default page 1 (offset 0) always counts, so a write (a new
7
+ * session, a triage move) shows at once where the user looks first. A list
8
+ * whose count is expensive and whose total may lag can pass
9
+ * `firstPageTtlMs`: page 1 then reuses a count younger than that, so
10
+ * dashboard tabs that poll the same list share one count. Later pages reuse
8
11
  * that count for 30 s. A cold later page (deep link, expired entry) counts
9
12
  * too, so `total` is always set.
10
13
  */
@@ -13,7 +16,7 @@ export const LIST_TOTAL_TTL_MS = 30_000;
13
16
  const MAX_ENTRIES = 500;
14
17
 
15
18
  interface Entry {
16
- expiresAt: number;
19
+ countedAt: number;
17
20
  total: Promise<number>;
18
21
  }
19
22
 
@@ -34,19 +37,23 @@ function filterKey(value: unknown): string {
34
37
  );
35
38
  }
36
39
 
37
- export function createListTotalCache(ttlMs = LIST_TOTAL_TTL_MS): ListTotalCache {
40
+ export function createListTotalCache(
41
+ ttlMs = LIST_TOTAL_TTL_MS,
42
+ firstPageTtlMs = 0,
43
+ ): ListTotalCache {
38
44
  const entries = new Map<string, Entry>();
39
45
  return {
40
46
  get(filters, offset, compute) {
41
47
  const key = filterKey(filters);
42
48
  const now = Date.now();
43
49
  const hit = entries.get(key);
44
- if (hit && hit.expiresAt > now && (offset ?? 0) > 0) return hit.total;
50
+ const maxAge = (offset ?? 0) > 0 ? ttlMs : Math.min(ttlMs, firstPageTtlMs);
51
+ if (hit && hit.countedAt + maxAge > now) return hit.total;
45
52
  if (hit) entries.delete(key);
46
53
 
47
- // Cache the promise, so a later page that arrives mid-count shares it.
54
+ // Cache the promise, so a request that arrives mid-count shares it.
48
55
  const total = compute();
49
- entries.set(key, { expiresAt: now + ttlMs, total });
56
+ entries.set(key, { countedAt: now, total });
50
57
  total.catch(() => {
51
58
  if (entries.get(key)?.total === total) entries.delete(key);
52
59
  });
@@ -32,6 +32,10 @@ async function getBridgeServer(): Promise<McpServer> {
32
32
  return _bridgeServer;
33
33
  }
34
34
 
35
+ export function resetBridgeServerForTesting(): void {
36
+ _bridgeServer = null;
37
+ }
38
+
35
39
  type RegisteredTool = {
36
40
  handler: (argsOrExtra: unknown, extra?: unknown) => unknown | Promise<unknown>;
37
41
  inputSchema?: AnySchema;
@@ -1568,17 +1568,31 @@ const MEMORY_GC_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
1568
1568
  let memoryGcTimer: ReturnType<typeof setInterval> | null = null;
1569
1569
 
1570
1570
  const SEARCH_RETRIEVAL_TTL_DAYS = 90;
1571
+ /** Rows per purge statement. The purge yields the event loop between batches. */
1572
+ const SEARCH_RETRIEVAL_PURGE_BATCH = 2000;
1571
1573
 
1572
- async function purgeStaleSearchRetrievals(): Promise<number> {
1574
+ export async function purgeStaleSearchRetrievals(
1575
+ batchSize = SEARCH_RETRIEVAL_PURGE_BATCH,
1576
+ ): Promise<number> {
1573
1577
  try {
1574
1578
  const cutoff = new Date(
1575
1579
  Date.now() - SEARCH_RETRIEVAL_TTL_DAYS * 24 * 60 * 60 * 1000,
1576
1580
  ).toISOString();
1577
- const result = await getDbClient().run(
1578
- "DELETE FROM memory_retrieval WHERE eventType = 'search' AND retrievedAt < ?",
1579
- [cutoff],
1580
- );
1581
- return result.changes;
1581
+ let purged = 0;
1582
+ for (;;) {
1583
+ // `+eventType` keeps the planner on the retrievedAt range. Without
1584
+ // sqlite_stat1 it prefers the eventType equality, which reads every
1585
+ // search row to test its date, even when nothing is stale.
1586
+ const result = await getDbClient().run(
1587
+ `DELETE FROM memory_retrieval WHERE rowid IN (
1588
+ SELECT rowid FROM memory_retrieval
1589
+ WHERE +eventType = 'search' AND retrievedAt < ? LIMIT ?)`,
1590
+ [cutoff, batchSize],
1591
+ );
1592
+ purged += result.changes;
1593
+ if (result.changes < batchSize) return purged;
1594
+ await new Promise<void>((resolve) => setImmediate(resolve));
1595
+ }
1582
1596
  } catch (err) {
1583
1597
  console.error("[memory-gc] Search retrieval purge failed:", (err as Error).message);
1584
1598
  return 0;
@@ -1602,11 +1616,22 @@ async function runMemoryGcTick(label: "Initial" | "Periodic"): Promise<void> {
1602
1616
  }
1603
1617
  }
1604
1618
 
1605
- export async function startMemoryGc(intervalMs = MEMORY_GC_INTERVAL_MS): Promise<void> {
1619
+ /** The first purge waits this long, so it does not compete with boot work. */
1620
+ const MEMORY_GC_INITIAL_DELAY_MS = 2 * 60 * 1000;
1621
+ let memoryGcInitialTimer: ReturnType<typeof setTimeout> | null = null;
1622
+
1623
+ export async function startMemoryGc(
1624
+ intervalMs = MEMORY_GC_INTERVAL_MS,
1625
+ initialDelayMs = MEMORY_GC_INITIAL_DELAY_MS,
1626
+ ): Promise<void> {
1606
1627
  if (memoryGcTimer) return;
1607
1628
 
1608
- // Run immediately on startup to clear any backlog
1609
- await runMemoryGcTick("Initial");
1629
+ // Clear any backlog soon after startup, but off the boot path.
1630
+ memoryGcInitialTimer = setTimeout(() => {
1631
+ memoryGcInitialTimer = null;
1632
+ void runMemoryGcTick("Initial");
1633
+ }, initialDelayMs);
1634
+ if (typeof memoryGcInitialTimer?.unref === "function") memoryGcInitialTimer.unref();
1610
1635
 
1611
1636
  memoryGcTimer = setInterval(() => {
1612
1637
  void runMemoryGcTick("Periodic");
@@ -1615,6 +1640,10 @@ export async function startMemoryGc(intervalMs = MEMORY_GC_INTERVAL_MS): Promise
1615
1640
  }
1616
1641
 
1617
1642
  export function stopMemoryGc(): void {
1643
+ if (memoryGcInitialTimer) {
1644
+ clearTimeout(memoryGcInitialTimer);
1645
+ memoryGcInitialTimer = null;
1646
+ }
1618
1647
  if (memoryGcTimer) {
1619
1648
  clearInterval(memoryGcTimer);
1620
1649
  memoryGcTimer = null;
@@ -13,10 +13,12 @@ import {
13
13
  getLatestScriptRunStepTaskByContextKey,
14
14
  getScriptRun,
15
15
  getScriptRunByIdempotencyKey,
16
+ getScriptRunFollowUpGuard,
16
17
  getScriptRunJournalStep,
17
18
  getTaskById,
18
19
  listScriptRunJournalSteps,
19
20
  listScriptRuns,
21
+ setScriptRunFollowUpGuard,
20
22
  updateScriptRun,
21
23
  updateScriptRunIfNotTerminal,
22
24
  updateScriptRunIfRunning,
@@ -24,6 +26,7 @@ import {
24
26
  } from "../be/db";
25
27
  import { explicitModelErrorForAgent } from "../be/model-validation";
26
28
  import { refuseSourceWithSecrets } from "../be/scripts/source-secrets";
29
+ import { can } from "../rbac";
27
30
  import { lintWorkflowLabels } from "../script-workflows/label-lint";
28
31
  import { scriptRunMaxAgentTasks, scriptRunMaxSteps } from "../script-workflows/limits";
29
32
  import {
@@ -32,6 +35,12 @@ import {
32
35
  terminateScriptRunProcess,
33
36
  } from "../script-workflows/supervisor";
34
37
  import {
38
+ evaluateFollowUpRedelegation,
39
+ type FollowUpRedelegationBlock,
40
+ followUpThreadForSourceTask,
41
+ } from "../tasks/follow-up-redelegation-guard";
42
+ import {
43
+ type AgentTask,
35
44
  AgentTaskStatusSchema,
36
45
  RoutingReasonSchema,
37
46
  ScriptRunJournalEntrySchema,
@@ -111,6 +120,8 @@ const agentTaskBodySchema = z.object({
111
120
  parentTaskId: z.string().uuid().optional(),
112
121
  requestedByUserId: z.string().optional(),
113
122
  outputSchema: z.record(z.string(), z.unknown()).optional(),
123
+ /** Dispatch even when the follow-up re-delegation guard blocks it (same flag as send-task). */
124
+ allowRedelegation: z.boolean().optional(),
114
125
  });
115
126
 
116
127
  const scriptRunCreatedSchema = z.object({
@@ -298,10 +309,70 @@ const agentTaskRoute = route({
298
309
  description: "Agent task created or still running",
299
310
  schema: z.object({ taskId: z.string(), status: AgentTaskStatusSchema }),
300
311
  },
312
+ 403: {
313
+ description:
314
+ "Script run belongs to another agent, or the run's agent may not create a child task under parentTaskId",
315
+ },
301
316
  404: { description: "Script run not found" },
317
+ 409: { description: "Agent task failed, was cancelled, or was superseded" },
318
+ 422: { description: "Blocked by the follow-up re-delegation guard" },
302
319
  },
303
320
  });
304
321
 
322
+ /**
323
+ * Send-task's task.parent.attach decision for a `ctx.step.agentTask` step: a
324
+ * step inherits its parent's contextKey (its Slack or chat thread) and
325
+ * requester, so the run's agent needs the right to hold a child under it.
326
+ * Returns the 403 message, or null when allowed. A parent id with no task row
327
+ * passes: a step under it inherits nothing, as on send-task.
328
+ */
329
+ async function stepParentRefusal(runAgentId: string, parentTaskId: string): Promise<string | null> {
330
+ const parentTask = await getTaskById(parentTaskId);
331
+ if (!parentTask) return null;
332
+ const runAgent = await getAgentById(runAgentId);
333
+ const decision = can({
334
+ principal: { kind: "agent", agentId: runAgentId, isLead: runAgent?.isLead ?? false },
335
+ verb: "task.parent.attach",
336
+ resource: {
337
+ kind: "task",
338
+ taskId: parentTask.id,
339
+ requestedByUserId: parentTask.requestedByUserId,
340
+ creatorAgentId: parentTask.creatorAgentId,
341
+ agentId: parentTask.agentId,
342
+ },
343
+ source: "http",
344
+ });
345
+ return decision.allow
346
+ ? null
347
+ : `You don't have context for parent task ${parentTaskId}. Only its assignee, its creator, or a lead can create a child task under it.`;
348
+ }
349
+
350
+ /**
351
+ * The launching session's task (X-Source-Task-Id), kept only when it belongs
352
+ * to the launching agent so a run cannot borrow another agent's session.
353
+ */
354
+ async function resolveRunSourceTaskId(
355
+ req: IncomingMessage,
356
+ agentId: string,
357
+ ): Promise<string | undefined> {
358
+ const raw = req.headers["x-source-task-id"];
359
+ const sourceTaskId = Array.isArray(raw) ? raw[0] : raw;
360
+ if (!sourceTaskId) return undefined;
361
+ const task = await getTaskById(sourceTaskId);
362
+ return task?.agentId === agentId ? task.id : undefined;
363
+ }
364
+
365
+ /** One `ctx.step.agentTask` dispatch: the step task, or the guard rule that blocked it. */
366
+ type StepDispatch =
367
+ | {
368
+ task: AgentTask;
369
+ /** True when this call inserted the step, false on a replay. */
370
+ created?: boolean;
371
+ blocked?: undefined;
372
+ override?: FollowUpRedelegationBlock["rule"];
373
+ }
374
+ | { task?: undefined; blocked: FollowUpRedelegationBlock; override?: undefined };
375
+
305
376
  async function requireAgent(res: ServerResponse, agentId: string | undefined) {
306
377
  if (!agentId) {
307
378
  jsonError(res, "X-Agent-ID required for script runs API", 400);
@@ -408,6 +479,7 @@ export async function handleScriptRuns(
408
479
  // Cap check and insert commit together: N concurrent POSTs otherwise all
409
480
  // read cap-1 active runs and all insert, exceeding the cap.
410
481
  const cap = scriptRunConcurrencyCap();
482
+ const sourceTaskId = await resolveRunSourceTaskId(req, agent.id);
411
483
  const creation = await getDbClient().transaction(async () => {
412
484
  if ((await countActiveScriptRuns()) >= cap) return null;
413
485
  return await createScriptRun({
@@ -418,6 +490,7 @@ export async function handleScriptRuns(
418
490
  scriptName: parsed.body.scriptName,
419
491
  idempotencyKey: parsed.body.idempotencyKey,
420
492
  requestedByUserId: parsed.body.requestedByUserId,
493
+ sourceTaskId,
421
494
  createdBy: parsed.body.requestedByUserId,
422
495
  });
423
496
  });
@@ -629,9 +702,29 @@ export async function handleScriptRuns(
629
702
  return true;
630
703
  }
631
704
 
705
+ // Only the run's own harness dispatches its steps (it sends the run's
706
+ // agent id). Step tasks carry that agent as creator, so another agent
707
+ // must not create them.
708
+ if (agentId !== run.agentId) {
709
+ jsonError(res, "Script run belongs to another agent", 403);
710
+ return true;
711
+ }
712
+
713
+ // The requested parent, checked before any lookup or insert so a refused
714
+ // call creates nothing.
715
+ if (parsed.body.parentTaskId) {
716
+ const refusal = await stepParentRefusal(run.agentId, parsed.body.parentTaskId);
717
+ if (refusal) {
718
+ jsonError(res, refusal, 403);
719
+ return true;
720
+ }
721
+ }
722
+
632
723
  const contextKey = `script-run:${run.id}:${parsed.body.stepKey}`;
633
724
  let task = await getLatestScriptRunStepTaskByContextKey(contextKey);
725
+ let dispatchedHere = false;
634
726
  if (!task) {
727
+ const taskDescription = parsed.body.template ?? parsed.body.task ?? parsed.body.stepKey;
635
728
  const modelError = await explicitModelErrorForAgent({
636
729
  model: parsed.body.model,
637
730
  allowCustomModel: parsed.body.allowCustomModel,
@@ -641,10 +734,47 @@ export async function handleScriptRuns(
641
734
  jsonError(res, modelError, 400);
642
735
  return true;
643
736
  }
737
+ const followUpThread = await followUpThreadForSourceTask(run.sourceTaskId);
738
+ // The replay lookup, the guard decision and the insert share one write
739
+ // transaction. It serializes this step against the run's other steps,
740
+ // other runs and send-task dispatches into the same thread.
741
+ let dispatched: StepDispatch;
644
742
  try {
645
- task = await createTaskExtended(
646
- parsed.body.template ?? parsed.body.task ?? parsed.body.stepKey,
647
- {
743
+ dispatched = await getDbClient().transaction(async (): Promise<StepDispatch> => {
744
+ const replayed = await getLatestScriptRunStepTaskByContextKey(contextKey);
745
+ if (replayed) return { task: replayed };
746
+
747
+ // Follow-up re-delegation guard, as in send-task. A run is one
748
+ // dispatch: its first new step decides for the whole run, and every
749
+ // later step follows that decision. Later steps never compare against
750
+ // the run's own completed steps, a long run cannot use up the thread's
751
+ // loop cap, and parallel steps are dispatched all or none.
752
+ let override: FollowUpRedelegationBlock["rule"] | undefined;
753
+ if (followUpThread) {
754
+ const decision = await getScriptRunFollowUpGuard(run.id);
755
+ if (decision !== "passed") {
756
+ const blocked: FollowUpRedelegationBlock | null =
757
+ decision === null
758
+ ? await evaluateFollowUpRedelegation({
759
+ ...followUpThread,
760
+ creatorAgentId: run.agentId,
761
+ taskDescription,
762
+ targetAgentId: parsed.body.agentId,
763
+ })
764
+ : {
765
+ rule: decision,
766
+ message: `Blocked by the follow-up re-delegation guard (${decision} rule): an earlier step of this script run was blocked, and a run counts as one dispatch. If it must run, pass allowRedelegation: true.`,
767
+ };
768
+ if (blocked && !parsed.body.allowRedelegation) {
769
+ if (decision === null) await setScriptRunFollowUpGuard(run.id, blocked.rule);
770
+ return { blocked };
771
+ }
772
+ override = blocked?.rule;
773
+ await setScriptRunFollowUpGuard(run.id, "passed");
774
+ }
775
+ }
776
+
777
+ const created = await createTaskExtended(taskDescription, {
648
778
  agentId: parsed.body.agentId,
649
779
  routingReason:
650
780
  parsed.body.routingReason ?? (parsed.body.agentId ? "human_pinned" : undefined),
@@ -660,6 +790,20 @@ export async function handleScriptRuns(
660
790
  offeredTo: parsed.body.offerMode ? parsed.body.agentId : undefined,
661
791
  taskType: "script-run-step",
662
792
  source: "mcp",
793
+ // Counts the run toward the launcher's follow-up loop cap.
794
+ creatorAgentId: run.agentId,
795
+ // With no parent, the step inherits the launching session's Slack
796
+ // thread, as a send-task from that session does.
797
+ sourceTaskId: run.sourceTaskId,
798
+ // A guarded run's steps are its dispatches into the follow-up's
799
+ // thread, whatever their parent, so the guard sees them there.
800
+ ...(followUpThread
801
+ ? {
802
+ slackChannelId: followUpThread.channelId,
803
+ slackThreadTs: followUpThread.threadTs,
804
+ slackUserId: followUpThread.userId,
805
+ }
806
+ : {}),
663
807
  dir: parsed.body.dir,
664
808
  vcsRepo: parsed.body.vcsRepo,
665
809
  model: parsed.body.model,
@@ -667,8 +811,9 @@ export async function handleScriptRuns(
667
811
  requestedByUserId: parsed.body.requestedByUserId ?? run.requestedByUserId,
668
812
  outputSchema: parsed.body.outputSchema,
669
813
  contextKey,
670
- },
671
- );
814
+ });
815
+ return { task: created, created: true, override };
816
+ });
672
817
  } catch (error) {
673
818
  if (error instanceof ExtensionAgentAssignmentError) {
674
819
  jsonError(res, error.message, 400);
@@ -676,6 +821,30 @@ export async function handleScriptRuns(
676
821
  }
677
822
  throw error;
678
823
  }
824
+ if (dispatched.override) {
825
+ console.log(
826
+ `[script-runs] follow-up re-delegation override: rule=${dispatched.override} runId=${run.id} creatorAgentId=${run.agentId} sourceTaskId=${run.sourceTaskId} thread=${followUpThread?.channelId}/${followUpThread?.threadTs}`,
827
+ );
828
+ }
829
+ if (!dispatched.task) {
830
+ json(res, { error: dispatched.blocked.message, rule: dispatched.blocked.rule }, 422);
831
+ return true;
832
+ }
833
+ task = dispatched.task;
834
+ dispatchedHere = dispatched.created === true;
835
+ }
836
+
837
+ // A replay answers from the selected step, whatever parent this call sent
838
+ // (or omitted), so authorize the parent the step actually holds. Every
839
+ // replay lookup above lands here before any output is returned. A step this
840
+ // call just dispatched was authorized above; refusing it now would leave
841
+ // the work running behind a 403.
842
+ if (!dispatchedHere && task.parentTaskId) {
843
+ const refusal = await stepParentRefusal(run.agentId, task.parentTaskId);
844
+ if (refusal) {
845
+ jsonError(res, refusal, 403);
846
+ return true;
847
+ }
679
848
  }
680
849
 
681
850
  const deadline = Date.now() + 30_000;
@@ -45,6 +45,7 @@ import { getSlackConnectionState } from "../slack/connection-state";
45
45
  import { isTelemetryEnabled } from "../telemetry";
46
46
  import { type AgentCredStatus, AutomationIntegrationIdSchema, ProviderNameSchema } from "../types";
47
47
  import { isRuntimePoolsEnabled } from "../utils/runtime-pools-flag";
48
+ import { resolveAppUrl } from "./config-values";
48
49
  import { route } from "./route-def";
49
50
  import { json, jsonError } from "./utils";
50
51
 
@@ -116,6 +117,12 @@ export const StatusIdentitySchema = z.object({
116
117
  * cloud installs can be sliced server-side.
117
118
  */
118
119
  org_id: z.string().nullable(),
120
+ /**
121
+ * The swarm's public dashboard URL (`APP_URL`, then `DASHBOARD_URL`, with
122
+ * swarm_config precedence). Null when neither is set. The dashboard reads
123
+ * links to it in agent output as its own. Absent on older APIs.
124
+ */
125
+ dashboard_url: z.string().nullable().optional(),
119
126
  });
120
127
  export type StatusIdentity = z.infer<typeof StatusIdentitySchema>;
121
128
 
@@ -334,7 +341,7 @@ export async function rollupCredStatusForProvider(provider: string): Promise<Cre
334
341
 
335
342
  // ─── Identity ────────────────────────────────────────────────────────────────
336
343
 
337
- function buildIdentity(): StatusIdentity {
344
+ function buildIdentity(dashboardUrl: string | null): StatusIdentity {
338
345
  const cloudRaw = process.env.SWARM_CLOUD;
339
346
  const hideRaw = process.env.SWARM_HIDE_CLOUD_PROMO;
340
347
  return {
@@ -345,6 +352,7 @@ function buildIdentity(): StatusIdentity {
345
352
  marketing_url: process.env.SWARM_MARKETING_URL?.trim() || null,
346
353
  hide_cloud_promo: hideRaw === "true" || hideRaw === "1",
347
354
  org_id: process.env.SWARM_ORG_ID?.trim() || null,
355
+ dashboard_url: dashboardUrl,
348
356
  };
349
357
  }
350
358
 
@@ -759,7 +767,7 @@ export async function buildStatusPayload(): Promise<StatusResponse> {
759
767
  const instance = await getInstanceFeedbackSignals();
760
768
  return {
761
769
  runtime_pools: { enabled: isRuntimePoolsEnabled(), supervisor: runtimeSupervisorStatus() },
762
- identity: buildIdentity(),
770
+ identity: buildIdentity(await resolveAppUrl()),
763
771
  setup,
764
772
  activity: { ...activity, leads_ready: readyLeads },
765
773
  agent_fs: {
package/src/http/tasks.ts CHANGED
@@ -84,7 +84,7 @@ import {
84
84
  import { ensure } from "../utils/business-use";
85
85
  import { getRequestAuth } from "../utils/request-auth-context";
86
86
  import { scrubSecrets } from "../utils/secret-scrubber";
87
- import { createListTotalCache } from "./list-total-cache";
87
+ import { createListTotalCache, LIST_TOTAL_TTL_MS } from "./list-total-cache";
88
88
  import {
89
89
  agentFirstPrincipal,
90
90
  authUserId,
@@ -224,7 +224,9 @@ const SupersedeTaskResponseSchema = z.object({
224
224
 
225
225
  // ─── Route Definitions ───────────────────────────────────────────────────────
226
226
 
227
- const taskTotals = createListTotalCache();
227
+ // The task list total scans every visible task, and only the pager shows it,
228
+ // so page 1 polls reuse a total younger than 15 s.
229
+ const taskTotals = createListTotalCache(LIST_TOTAL_TTL_MS, 15_000);
228
230
 
229
231
  const listTasks = route({
230
232
  method: "get",
@@ -13,6 +13,7 @@ import {
13
13
  } from "@/be/db";
14
14
  import { rememberSessionLabel } from "@/be/db/session-labels";
15
15
  import { resolveIdentity } from "@/be/identity";
16
+ import { SteeringBlockedError } from "@/be/steering";
16
17
  import { recordUnmappedIdentity } from "@/be/unmapped-identities";
17
18
  import { dispatchPre } from "@/extensions/dispatcher";
18
19
  import { resolveTemplate } from "@/prompts/resolver";
@@ -154,11 +155,20 @@ export async function handleChatInbound(
154
155
  // A thread with messages already waiting keeps its order: append, do not steer past them.
155
156
  // Another sender's message takes the buffer, so it gets its own header and requester.
156
157
  if (!immediate && attachments.length === 0 && text && !isChatThreadBuffered(thread.id)) {
157
- const steering = await requestChatThreadSteering({
158
- contextKey: chatSdkContextKey(thread.id),
159
- message: text,
160
- resolveSender: () => resolveChatSender(adapter, message.author.userId),
161
- });
158
+ let steering: Awaited<ReturnType<typeof requestChatThreadSteering>>;
159
+ try {
160
+ steering = await requestChatThreadSteering({
161
+ contextKey: chatSdkContextKey(thread.id),
162
+ message: text,
163
+ resolveSender: () => resolveChatSender(adapter, message.author.userId),
164
+ });
165
+ } catch (error) {
166
+ if (!(error instanceof SteeringBlockedError)) throw error;
167
+ console.info(`[chat-sdk] ${thread.id}: steering blocked: ${error.reason}`);
168
+ stopTypingKeepalive(thread.id);
169
+ await setChatMessageReaction(adapter, thread.id, message.id, "failed");
170
+ return;
171
+ }
162
172
  if (steering) {
163
173
  console.log(
164
174
  `[chat-sdk] ${adapter} message steered task ${steering.task.id} (${steering.result.outcome})`,
@@ -1,5 +1,5 @@
1
1
  import { getLatestLeadTaskByContextKey } from "@/be/db";
2
- import { requestSteering, SteeringRequestError } from "@/be/steering";
2
+ import { requestSteering, SteeringBlockedError, SteeringRequestError } from "@/be/steering";
3
3
  import type { AgentTask, SteerResult } from "@/types";
4
4
  import { scrubSecrets } from "@/utils/secret-scrubber";
5
5
  import { isSteeringEnabled } from "@/utils/steering-enabled";
@@ -44,6 +44,8 @@ export async function requestChatThreadSteering(args: {
44
44
  });
45
45
  return { task, result };
46
46
  } catch (error) {
47
+ // A policy block must not fall back to task creation.
48
+ if (error instanceof SteeringBlockedError) throw error;
47
49
  const detail = error instanceof Error ? error.message : String(error);
48
50
  const what = error instanceof SteeringRequestError ? "was refused" : "failed";
49
51
  console.warn(
@@ -12,6 +12,8 @@ const RESERVED_FAMILIES = new Map([
12
12
  ["worker", "namespace is reserved for worker nudges"],
13
13
  // Durable dedupe for SLACK_BLOCK_EXTERNAL_CHANNELS alerts (src/slack/external-channel.ts).
14
14
  ["slack-external-alert", "namespace is reserved for Slack external channel alerts"],
15
+ // Threads the swarm left via slack-leave-thread (src/slack/left-threads.ts).
16
+ ["slack-left-thread", "namespace is reserved for left Slack threads; use slack-leave-thread"],
15
17
  // Source group labels (src/be/db/session-labels.ts). Private, see PRIVATE_FAMILIES.
16
18
  [
17
19
  "session-labels",
@@ -78,12 +78,15 @@ const REPO_CLAUDE_MD_INLINE_PROVIDERS: ReadonlySet<string> = new Set(["opencode"
78
78
  /**
79
79
  * Template event for the tool-discovery line. Amp, Codex and OpenCode run MCP
80
80
  * tools inside their own code sandbox (`code_exec`, `exec`, `execute`), each
81
- * with its own call shape, so the generic lines are wrong for them.
81
+ * with its own call shape, and fx defers them behind `capability_search` under
82
+ * `mcp_<server>_<tool>` names, so the generic lines are wrong for them.
82
83
  */
83
84
  function toolDiscoveryEvent(provider: string, hasToolSearch: boolean): string {
84
85
  switch (provider) {
85
86
  case "amp":
86
87
  return "system.agent.tool_discovery.amp";
88
+ case "fx":
89
+ return "system.agent.tool_discovery.fx";
87
90
  case "codex":
88
91
  return "system.agent.tool_discovery.codex";
89
92
  case "opencode":
@@ -314,6 +314,7 @@ The engine posts the thread tree and the outcome card. You post at most one mess
314
314
  The outcome card publishes your \`output\` verbatim. Keep free-text \`output\` under 120 words by default, with the exceptions in How you write.
315
315
  A Slack task from an unknown user: register them with \`manage-user\` first.
316
316
  You MUST use the \`slack-interaction\` skill before you post to Slack.
317
+ Call \`slack-leave-thread\` with your taskId when a thread no longer needs you: people are talking among themselves, someone tells you to stop or back off, or the thread's work is done. The swarm then ignores the thread until someone @mentions it. Leave quietly, with no goodbye message.
317
318
  `,
318
319
  variables: [],
319
320
  category: "system",
@@ -397,6 +398,21 @@ registerTemplate({
397
398
  category: "system",
398
399
  });
399
400
 
401
+ /**
402
+ * Last line of an fx system prompt that exceeds the 64 KiB `systemPrompt` cap.
403
+ * The adapter sends the first part and writes the whole prompt to a file.
404
+ */
405
+ registerTemplate({
406
+ eventType: "system.agent.fx.system_prompt_file",
407
+ header: "",
408
+ defaultBody:
409
+ "These instructions are cut short. Read {{path}} before you start: it holds the complete swarm instructions, of which the text above is only the first part.",
410
+ variables: [
411
+ { name: "path", description: "Absolute path of the file with the full system prompt" },
412
+ ],
413
+ category: "system",
414
+ });
415
+
400
416
  /**
401
417
  * First message of a cursor session on the first-message path. Cursor keeps
402
418
  * its own system prompt there, so the swarm's rides on top of the task prompt.
@@ -448,6 +464,15 @@ registerTemplate({
448
464
  category: "system",
449
465
  });
450
466
 
467
+ registerTemplate({
468
+ eventType: "system.agent.tool_discovery.fx",
469
+ header: "",
470
+ defaultBody:
471
+ "Most swarm tools are deferred behind `capability_search`. Each one is named `mcp_swarm_<tool>`, so `store-progress` is `mcp_swarm_store-progress`. Call a tool you know by that name directly. To find one, use `capability_search`.",
472
+ variables: [],
473
+ category: "system",
474
+ });
475
+
451
476
  registerTemplate({
452
477
  eventType: "system.agent.tool_discovery.codex",
453
478
  header: "",
@@ -574,6 +599,7 @@ registerTemplate({
574
599
  ## Slack (scripts-only)
575
600
 
576
601
  This task originated from Slack (channel: \`{{slackChannelId}}\`). The engine maintains the thread tree and publishes the top-level outcome card. Post at most one distinct agent-authored message per task, concrete, sized to what the user asked for. Progress, start, completion, failure, acknowledgment messages, and relayed raw task output stay out of Slack. Named Slack tools are not exposed in scripts-only mode, so use \`script-run\` with inline source calling \`ctx.swarm.slack_reply({ taskId, message })\` (your taskId carries the thread context).
602
+ When the thread no longer needs you (people are talking among themselves, someone tells you to stop, or the work is done), call \`ctx.swarm.slack_leaveThread({ taskId })\` the same way. The swarm then ignores the thread until someone @mentions it. Leave quietly, with no goodbye message.
577
603
  The outcome card publishes your \`output\` verbatim. Keep free-text \`output\` under 120 words by default, with the exceptions in How you write.
578
604
  `,
579
605
  variables: [