@desplega.ai/agent-swarm 2.5.3 → 2.6.0

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 (349) hide show
  1. package/README.md +2 -2
  2. package/dist/{acp-adapter-gr2fcyyj.js → acp-adapter-dgfakfpw.js} +9 -9
  3. package/dist/{actions-wv766ghf.js → actions-fbqma8aa.js} +21 -21
  4. package/dist/{amp-adapter-speb906t.js → amp-adapter-en0jy6hs.js} +4 -4
  5. package/dist/{app-ag29n14f.js → app-k8a5cgpp.js} +3 -3
  6. package/dist/{assistant-3x8bz3eg.js → assistant-a16433gr.js} +30 -26
  7. package/dist/{boot-reembed-jsmfrpzf.js → boot-reembed-6c1sjfnr.js} +1 -1
  8. package/dist/{boot-reembed-5b3d2eea.js → boot-reembed-f4nn561n.js} +1 -1
  9. package/dist/{boot-scrub-sweep-s73gerxy.js → boot-scrub-sweep-d4w9gajf.js} +4 -4
  10. package/dist/{boot-seeds-5zmax2wr.js → boot-seeds-9bmkspyw.js} +4 -4
  11. package/dist/{claude-adapter-hswdnm3t.js → claude-adapter-nys1kjxa.js} +9 -9
  12. package/dist/{claude-managed-adapter-cvbzj1h6.js → claude-managed-adapter-55207973.js} +3 -3
  13. package/dist/{claude-managed-setup-3k8m3hhc.js → claude-managed-setup-3cm35gey.js} +11 -4
  14. package/dist/{claude-sdk-session-fkbyk66x.js → claude-sdk-session-nw5yd8cp.js} +9313 -9260
  15. package/dist/{cli-210xd1f4.js → cli-03gg9pd1.js} +4 -4
  16. package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
  17. package/dist/cli-1tem6ns9.js +894 -0
  18. package/dist/{cli-25j01dpj.js → cli-24j1jeeb.js} +3 -3
  19. package/dist/{cli-ta8bd7th.js → cli-2sjhpsns.js} +2 -2
  20. package/dist/{cli-p859hcc8.js → cli-39aet655.js} +2 -2
  21. package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
  22. package/dist/{cli-e2ar4bp1.js → cli-3aqd4jaa.js} +121 -47
  23. package/dist/{cli-jjznk3f7.js → cli-3fwqy71j.js} +69 -35
  24. package/dist/{cli-p9rafdrb.js → cli-3gfvmzht.js} +7 -7
  25. package/dist/{cli-03vpq45j.js → cli-3ndmpxxv.js} +2 -2
  26. package/dist/{cli-aecdcr77.js → cli-4hxrf0p4.js} +228 -74
  27. package/dist/{cli-mz9afxxf.js → cli-4rf0f5vg.js} +15 -3
  28. package/dist/{cli-qzme5949.js → cli-5rj83bam.js} +284 -202
  29. package/dist/{cli-fsdprwrg.js → cli-5sckpqcx.js} +2 -2
  30. package/dist/{cli-cz5nmdxt.js → cli-683y0b3a.js} +2 -2
  31. package/dist/{cli-3ngdk5te.js → cli-6y51ryva.js} +2 -2
  32. package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
  33. package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
  34. package/dist/{cli-gx4rm0qx.js → cli-7enjwya9.js} +1 -1
  35. package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
  36. package/dist/{cli-avdvgcay.js → cli-9abdgn5t.js} +28 -9
  37. package/dist/{cli-99xgw26r.js → cli-a2rr0fmg.js} +1 -1
  38. package/dist/{cli-e5bxnjps.js → cli-a34pr3w1.js} +7 -7
  39. package/dist/{cli-502eqv93.js → cli-abkm71vr.js} +1 -1
  40. package/dist/{cli-mmhj8vd7.js → cli-ag2yrnt5.js} +11 -2
  41. package/dist/{cli-n45mr7zh.js → cli-aj8djyqt.js} +5 -5
  42. package/dist/{cli-1fc85mvt.js → cli-art7c1zh.js} +5 -5
  43. package/dist/{cli-40jsnrjm.js → cli-az25fn5f.js} +10 -8
  44. package/dist/{cli-9s05422w.js → cli-bqztfyve.js} +1 -1
  45. package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
  46. package/dist/{cli-svrj71pg.js → cli-c2habvsf.js} +12 -12
  47. package/dist/{cli-s49hpn37.js → cli-ca6banwz.js} +85 -19
  48. package/dist/{cli-hhxjnjzs.js → cli-cz37jqss.js} +12 -10
  49. package/dist/{cli-6n8axjv1.js → cli-d2dpf95n.js} +2 -2
  50. package/dist/{cli-tx00c0h6.js → cli-dqygeeh5.js} +17 -1
  51. package/dist/{cli-z21whrrg.js → cli-emhjcj2y.js} +17 -17
  52. package/dist/{cli-e8jyq7d8.js → cli-esakngh8.js} +7 -3
  53. package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
  54. package/dist/{cli-35gtk1p6.js → cli-ffvsj2qw.js} +44 -13
  55. package/dist/{cli-ce0ak9ms.js → cli-g20jjwt4.js} +26 -26
  56. package/dist/{cli-kxawjrxg.js → cli-g8ka5j4j.js} +772 -194
  57. package/dist/{cli-vwn81jbs.js → cli-gf95q3bm.js} +5 -5
  58. package/dist/{cli-s53vp709.js → cli-gnsb1zxa.js} +2 -2
  59. package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
  60. package/dist/{cli-gpv60278.js → cli-hfeb68z4.js} +7 -7
  61. package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
  62. package/dist/{cli-ewm3pwbq.js → cli-k21gx5ht.js} +46 -23
  63. package/dist/{cli-egjggehm.js → cli-kf4jree9.js} +2 -2
  64. package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
  65. package/dist/{cli-p0rzjkya.js → cli-m5a9m0ak.js} +32 -13
  66. package/dist/{cli-fzcn3d87.js → cli-m70bajzr.js} +3 -3
  67. package/dist/{cli-w1zwctj2.js → cli-mtnj4fwa.js} +11 -5
  68. package/dist/{cli-gpsz7g88.js → cli-mvjz5an3.js} +16 -12
  69. package/dist/{cli-hcpj2xqz.js → cli-n7asmce6.js} +225 -3
  70. package/dist/{cli-y561h5a9.js → cli-nn6vx2mq.js} +5 -5
  71. package/dist/cli-nq677csy.js +140 -0
  72. package/dist/{cli-a5ebc95n.js → cli-pfk92eqz.js} +18 -15
  73. package/dist/{cli-p6zs9h4y.js → cli-rr7zx19j.js} +767 -249
  74. package/dist/{cli-qgf8emrn.js → cli-sqma6vw1.js} +10 -7
  75. package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
  76. package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
  77. package/dist/{cli-qqzmjxkf.js → cli-v49fkghp.js} +11 -6
  78. package/dist/{cli-x77gen2p.js → cli-w2656ka1.js} +7 -7
  79. package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
  80. package/dist/{cli-8s5wx3nz.js → cli-zqne1qcj.js} +44 -17
  81. package/dist/cli.js +22 -22
  82. package/dist/{codex-adapter-n8ka66vf.js → codex-adapter-d95q4qsn.js} +7 -7
  83. package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
  84. package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
  85. package/dist/{codex-session-runner-fsebts37.js → codex-session-runner-gdwd8h0c.js} +8 -8
  86. package/dist/{commands-t04rt2pc.js → commands-jqmgk54b.js} +2 -2
  87. package/dist/{controller-rgna04r2.js → controller-z3jq589f.js} +30 -4
  88. package/dist/{controllers-connect-bqm1rtft.js → controllers-connect-r2za39ef.js} +23 -23
  89. package/dist/{cursor-adapter-swr2k95y.js → cursor-adapter-4mr4jgkw.js} +2 -3
  90. package/dist/{db-a4m7haw0.js → db-ppv2tze7.js} +3 -3
  91. package/dist/{db-24ck09xy.js → db-w36wwb5g.js} +4 -2
  92. package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
  93. package/dist/{dispatcher-nwf9ss0q.js → dispatcher-pv5h77vf.js} +20 -20
  94. package/dist/{docker-nfy2gc8t.js → docker-fdzyewpz.js} +4 -4
  95. package/dist/{dsh-adapter-kpdnhjst.js → dsh-adapter-d4pdpb3r.js} +2 -3
  96. package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
  97. package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
  98. package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-4sp4dvmc.js} +1 -1
  99. package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
  100. package/dist/{execute-0xpz8950.js → execute-jev839y1.js} +1 -1
  101. package/dist/{external-channel-j1e2p34p.js → external-channel-y6nkzhnn.js} +2 -2
  102. package/dist/{grok-adapter-qtjz0whk.js → grok-adapter-t08b9ntm.js} +10 -10
  103. package/dist/handlers-eqsnmv95.js +41 -0
  104. package/dist/{heartbeat-vtesda5d.js → heartbeat-vn31xwdk.js} +23 -23
  105. package/dist/{hook-b2hhj6rd.js → hook-6d0zbvvt.js} +6 -5
  106. package/dist/{hook-8wqaszda.js → hook-cpjs8q51.js} +13 -7
  107. package/dist/{http-mxcqhhcs.js → http-9c6medda.js} +1085 -136
  108. package/dist/{index-spwxjgay.js → index-38t35h0n.js} +20 -20
  109. package/dist/{index-1r8wfpzs.js → index-g6k9tctj.js} +1 -1
  110. package/dist/{index-srg522m6.js → index-k0yt1tvq.js} +20 -20
  111. package/dist/{index-31ceyf42.js → index-pkma1yf8.js} +1 -1
  112. package/dist/{index-n1vbvta9.js → index-qhjf2htm.js} +29 -24
  113. package/dist/index-yqgbfgaj.js +43 -0
  114. package/dist/{keepalive-dsd8rtgx.js → keepalive-em0117bp.js} +4 -4
  115. package/dist/lead-23e7z59t.js +79 -0
  116. package/dist/{maintenance-jqmqwnet.js → maintenance-kbme3qy9.js} +1 -1
  117. package/dist/{oauth-refresh-sweep-q2c3bnvm.js → oauth-refresh-sweep-xysj4yse.js} +3 -3
  118. package/dist/{onboard-w4yd7s8t.js → onboard-p6q0vz97.js} +3 -3
  119. package/dist/{opencode-adapter-9tppvxar.js → opencode-adapter-esv3k9yn.js} +7 -7
  120. package/dist/{otel-impl-f36byzbd.js → otel-impl-54qhqh1z.js} +2 -2
  121. package/dist/{pi-mono-adapter-tqsge1ay.js → pi-mono-adapter-mymbwhj0.js} +41 -14
  122. package/dist/{pricing-refresh-xvak5p4v.js → pricing-refresh-yb75wv33.js} +3 -3
  123. package/dist/{rbac-roles-gfn30fb8.js → rbac-roles-1vp2mw4p.js} +2 -2
  124. package/dist/{rbac-roles-682k2mt5.js → rbac-roles-dexhc9x8.js} +1 -1
  125. package/dist/render-v2-tjay5hhf.js +16 -0
  126. package/dist/{resume-2dvq348g.js → resume-8ayf37g5.js} +11 -9
  127. package/dist/{runtime-bt5pkpm1.js → runtime-hqe5cqcd.js} +1 -1
  128. package/dist/{session-access-7f21ekq9.js → session-access-fdhp987s.js} +4 -4
  129. package/dist/session-source-backfill-qgtc86z1.js +110 -0
  130. package/dist/{setup-epffxtbk.js → setup-4rrmqcq3.js} +1 -1
  131. package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-52km00d6.js} +2 -2
  132. package/dist/{work-objects-xz1a5rqk.js → work-objects-p28fgd70.js} +23 -23
  133. package/dist/worker-e3w7vye1.js +79 -0
  134. package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
  135. package/openapi.json +1323 -16
  136. package/package.json +5 -5
  137. package/plugin/commands/create-pr.md +2 -2
  138. package/plugin/pi-skills/create-pr/SKILL.md +2 -2
  139. package/src/assets/source-key.ts +95 -0
  140. package/src/be/db/api-keys.ts +80 -0
  141. package/src/be/db/approvals.ts +48 -0
  142. package/src/be/db/session-labels.ts +28 -0
  143. package/src/be/db/session-moves.ts +5 -6
  144. package/src/be/db/session-tree.ts +153 -14
  145. package/src/be/db/tasks/read.ts +11 -0
  146. package/src/be/db/tasks/write.ts +21 -6
  147. package/src/be/db/workflow-idempotency.ts +183 -0
  148. package/src/be/db/workflow-runs.ts +500 -7
  149. package/src/be/db-retention.ts +61 -6
  150. package/src/be/db.ts +126 -131
  151. package/src/be/memory/providers/openai-embedding.ts +5 -1
  152. package/src/be/migrations/242_workflow_idempotency_keys.sql +40 -0
  153. package/src/be/migrations/244_api_key_validation.sql +4 -0
  154. package/src/be/onboarding.ts +4 -0
  155. package/src/be/scripts/typecheck.ts +1 -1
  156. package/src/be/sealed-json.ts +1 -0
  157. package/src/be/seed-scripts/catalog/compound-insights.inline.ts +2 -2
  158. package/src/be/seed-scripts/catalog/compound-insights.ts +2 -2
  159. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  160. package/src/be/session-source-backfill.ts +163 -0
  161. package/src/be/sql-connections.ts +7 -1
  162. package/src/be/swarm-config-guard.ts +3 -0
  163. package/src/be/users.ts +9 -0
  164. package/src/comb/review-batch.ts +28 -1
  165. package/src/comb/templates.ts +7 -2
  166. package/src/commands/claude-managed-setup.ts +7 -1
  167. package/src/commands/controller.ts +33 -0
  168. package/src/commands/credential-refresh.ts +76 -1
  169. package/src/commands/credential-wait.ts +2 -2
  170. package/src/commands/provider-credentials.ts +210 -170
  171. package/src/commands/runner.ts +31 -6
  172. package/src/extensions/apply-task-create.ts +64 -11
  173. package/src/extensions/contract-types.generated.ts +1 -1
  174. package/src/extensions/contract.ts +30 -0
  175. package/src/extensions/dispatcher.ts +4 -0
  176. package/src/extensions/post-bridge.ts +8 -0
  177. package/src/hooks/context-bound-note.ts +196 -0
  178. package/src/hooks/hook.ts +12 -0
  179. package/src/http/all-routes.ts +2 -0
  180. package/src/http/api-keys.ts +53 -0
  181. package/src/http/assets.ts +15 -0
  182. package/src/http/codex-oauth-accounts.ts +191 -0
  183. package/src/http/comb.ts +15 -2
  184. package/src/http/config.ts +49 -17
  185. package/src/http/core.ts +6 -0
  186. package/src/http/fleet-benchmark.ts +5 -2
  187. package/src/http/idempotency-key.ts +117 -0
  188. package/src/http/index.ts +11 -0
  189. package/src/http/integrations.ts +5 -1
  190. package/src/http/kv.ts +8 -3
  191. package/src/http/onboarding.ts +6 -1
  192. package/src/http/pool-keys.ts +354 -0
  193. package/src/http/sessions.ts +37 -12
  194. package/src/http/stats.ts +1 -0
  195. package/src/http/status.ts +26 -12
  196. package/src/http/tasks.ts +3 -0
  197. package/src/http/used-by.ts +7 -2
  198. package/src/http/workflows.ts +207 -33
  199. package/src/integrations/chat-sdk/inbound.ts +27 -0
  200. package/src/kv-reserved-namespaces.ts +19 -0
  201. package/src/prompts/context-bound-note.ts +48 -0
  202. package/src/prompts/session-templates.ts +25 -1
  203. package/src/providers/acp-targets.ts +1 -1
  204. package/src/providers/amp-adapter.ts +30 -8
  205. package/src/providers/claude-adapter.ts +34 -5
  206. package/src/providers/claude-managed-adapter.ts +42 -9
  207. package/src/providers/claude-sdk-session.ts +10 -0
  208. package/src/providers/claude-session-events.ts +13 -0
  209. package/src/providers/codex-adapter.ts +83 -11
  210. package/src/providers/opencode-adapter.ts +18 -0
  211. package/src/providers/pi-mono-adapter.ts +35 -0
  212. package/src/runtime-providers/docker.ts +6 -2
  213. package/src/scheduler/scheduler.ts +3 -2
  214. package/src/scripts-runtime/swarm-sdk.ts +7 -1
  215. package/src/scripts-runtime/types/stdlib.d.ts +1 -1
  216. package/src/scripts-runtime/types/swarm-extension.d.ts +52 -8
  217. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -1
  218. package/src/slack/assistant.ts +7 -0
  219. package/src/slack/channel-labels.ts +74 -0
  220. package/src/slack/handlers.ts +8 -0
  221. package/src/slack/inbound-files.ts +5 -2
  222. package/src/slack/thread-buffer.ts +4 -0
  223. package/src/tasks/sibling-awareness.ts +12 -3
  224. package/src/telemetry-context.ts +5 -6
  225. package/src/telemetry-identity.ts +3 -3
  226. package/src/tests/amp-adapter.test.ts +97 -0
  227. package/src/tests/api-keys-validation.test.ts +170 -0
  228. package/src/tests/approval-requests.test.ts +10 -7
  229. package/src/tests/approval-sweeps.test.ts +12 -6
  230. package/src/tests/base-prompt.test.ts +1 -1
  231. package/src/tests/claude-hook-output.test.ts +79 -1
  232. package/src/tests/claude-managed-adapter.test.ts +321 -0
  233. package/src/tests/claude-routes-e2e.test.ts +7 -2
  234. package/src/tests/codex-adapter.test.ts +163 -0
  235. package/src/tests/codex-credential-pools.test.ts +120 -0
  236. package/src/tests/codex-gateway-route.test.ts +165 -0
  237. package/src/tests/codex-oauth-accounts.test.ts +357 -0
  238. package/src/tests/comb-review-batch.test.ts +63 -2
  239. package/src/tests/config-pool-keys.test.ts +533 -0
  240. package/src/tests/context-bound-note.test.ts +269 -0
  241. package/src/tests/credential-check.test.ts +290 -2
  242. package/src/tests/credential-refresh.test.ts +172 -1
  243. package/src/tests/db-retention.test.ts +9 -3
  244. package/src/tests/db-task-read-contract.test.ts +1 -0
  245. package/src/tests/db-task-write-contract.test.ts +1 -3
  246. package/src/tests/docs-index-footprint.test.js +3 -3
  247. package/src/tests/extensions-pre-task.test.ts +325 -0
  248. package/src/tests/fetch-resolved-env.test.ts +17 -0
  249. package/src/tests/fixtures/extensions/record-blocks/hooks.ts +10 -0
  250. package/src/tests/fixtures/extensions/record-blocks/manifest.json +7 -0
  251. package/src/tests/fixtures/extensions/record-task-create/hooks.ts +9 -0
  252. package/src/tests/fixtures/extensions/record-task-create/manifest.json +7 -0
  253. package/src/tests/fixtures/fake-amp.ts +22 -8
  254. package/src/tests/fixtures/held-template-render.ts +42 -0
  255. package/src/tests/fixtures/workflow-idempotency-trigger.child.ts +73 -0
  256. package/src/tests/fleet-benchmark.test.ts +8 -0
  257. package/src/tests/get-workflow-run-tool.test.ts +98 -0
  258. package/src/tests/integrations-http.test.ts +32 -2
  259. package/src/tests/internal-ai/complete-structured.test.ts +37 -1
  260. package/src/tests/internal-ai/credentials.test.ts +23 -0
  261. package/src/tests/kv-http.test.ts +32 -0
  262. package/src/tests/kv-tool.test.ts +25 -0
  263. package/src/tests/multi-runtime-registration.test.ts +12 -12
  264. package/src/tests/onboarding.test.ts +108 -1
  265. package/src/tests/openai-embedding-credentials.test.ts +35 -0
  266. package/src/tests/openapi-route-headers.test.ts +2 -0
  267. package/src/tests/opencode-adapter.test.ts +134 -0
  268. package/src/tests/openrouter-attribution-call-sites.test.ts +2 -2
  269. package/src/tests/org-website.test.ts +36 -0
  270. package/src/tests/pi-mono-adapter.test.ts +105 -0
  271. package/src/tests/prompt-template-session.test.ts +4 -3
  272. package/src/tests/provider-key-probes.test.ts +181 -0
  273. package/src/tests/rbac-e2e-helpers.test.ts +88 -1
  274. package/src/tests/rbac-e2e-helpers.ts +36 -11
  275. package/src/tests/runner-push-delivery.test.ts +25 -0
  276. package/src/tests/runtime-controller-liveness.test.ts +133 -0
  277. package/src/tests/runtime-providers-docker.test.ts +2 -0
  278. package/src/tests/script-connections.test.ts +5 -0
  279. package/src/tests/script-run-config-scope.test.ts +91 -3
  280. package/src/tests/script-runs-retention.test.ts +212 -0
  281. package/src/tests/sdk-allowlist.test.ts +39 -0
  282. package/src/tests/session-logs.test.ts +70 -0
  283. package/src/tests/session-source-backfill.test.ts +429 -0
  284. package/src/tests/session-source-keys.test.ts +236 -0
  285. package/src/tests/session-source-labels.test.ts +419 -0
  286. package/src/tests/sessions-detail-batch.test.ts +2 -0
  287. package/src/tests/sessions-move.test.ts +131 -0
  288. package/src/tests/sessions-tree.test.ts +336 -2
  289. package/src/tests/slack-inbound-files.test.ts +103 -23
  290. package/src/tests/status.test.ts +101 -18
  291. package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
  292. package/src/tests/test-net.ts +31 -0
  293. package/src/tests/transcription.test.ts +8 -0
  294. package/src/tests/used-by-route.test.ts +11 -0
  295. package/src/tests/workflow-cascade-cancel.test.ts +1341 -0
  296. package/src/tests/workflow-foreach-supersede-retry.test.ts +488 -0
  297. package/src/tests/workflow-http-v2.test.ts +131 -0
  298. package/src/tests/workflow-idempotency.test.ts +915 -0
  299. package/src/tests/workflow-step-order.test.ts +92 -0
  300. package/src/tests/workflow-sub-workflow.test.ts +30 -0
  301. package/src/tools/get-tasks.ts +8 -0
  302. package/src/tools/kv/kv-read-auth.ts +4 -2
  303. package/src/tools/task-action.ts +1 -1
  304. package/src/tools/workflows/cancel-workflow-run.ts +1 -1
  305. package/src/tools/workflows/get-workflow-run.ts +14 -4
  306. package/src/types.ts +24 -0
  307. package/src/utils/company-profile.ts +51 -1
  308. package/src/utils/context-window.ts +8 -6
  309. package/src/utils/credentials.ts +5 -2
  310. package/src/utils/internal-ai/complete-structured.ts +5 -0
  311. package/src/utils/internal-ai/credentials.ts +4 -3
  312. package/src/utils/provider-key-probes.ts +328 -0
  313. package/src/utils/transcription.ts +4 -1
  314. package/src/workflows/approval-cancel.ts +15 -13
  315. package/src/workflows/approval-notifications.ts +5 -1
  316. package/src/workflows/checkpoint.ts +6 -3
  317. package/src/workflows/engine.ts +30 -8
  318. package/src/workflows/executors/foreach.ts +2 -2
  319. package/src/workflows/executors/sub-workflow.ts +8 -1
  320. package/src/workflows/foreach-driver.ts +10 -3
  321. package/src/workflows/foreach-join.ts +29 -6
  322. package/src/workflows/recovery.ts +6 -1
  323. package/src/workflows/resume.ts +168 -56
  324. package/src/workflows/retry-poller.ts +49 -3
  325. package/src/workflows/task-step-routing.ts +29 -7
  326. package/templates/ai-toolbox.manifest.json +8 -8
  327. package/templates/community/open-prompt-showreel/PLAYBOOK.md +5 -6
  328. package/templates/community/open-prompt-showreel/lead-prompt.md +1 -1
  329. package/templates/community/open-prompt-showreel/skills/open-prompt-showreel/SKILL.md +4 -4
  330. package/templates/skills/agent-browser/SKILL.md +4 -4
  331. package/templates/skills/agent-browser/content.md +4 -4
  332. package/templates/skills/code-quality/config.json +1 -1
  333. package/templates/skills/code-quality/content.md +2 -2
  334. package/templates/skills/delegate-work/content.md +29 -25
  335. package/templates/skills/delegate-work/files/scripts/codex-exec.sh +5 -5
  336. package/templates/skills/qa/content.md +2 -2
  337. package/templates/skills/researching/content.md +4 -0
  338. package/templates/skills/swarm-scripts/SKILL.md +1 -0
  339. package/templates/skills/swarm-scripts/content.md +1 -0
  340. package/templates/skills/work-on-task/SKILL.md +1 -1
  341. package/templates/skills/work-on-task/content.md +1 -1
  342. package/dist/cli-7421jzsw.js +0 -84
  343. package/dist/cli-e45e4m6b.js +0 -413
  344. package/dist/cli-zw88zjba.js +0 -307
  345. package/dist/handlers-19xr6dnz.js +0 -41
  346. package/dist/index-kw2mmat7.js +0 -43
  347. package/dist/lead-zyerk1x2.js +0 -79
  348. package/dist/render-v2-hgytz8nn.js +0 -12
  349. package/dist/worker-42t8emjt.js +0 -79
@@ -30,13 +30,22 @@
30
30
 
31
31
  import { existsSync as nodeExistsSync } from "node:fs";
32
32
  import { join } from "node:path";
33
+ import {
34
+ CODEX_GATEWAY_API_KEY,
35
+ deriveDefaultRoute,
36
+ type ModelRoute,
37
+ routeCredentialStatus,
38
+ routeUnsetEnv,
39
+ } from "@desplega/model-routing";
33
40
  import { buildRatingsFromLlm, fetchRetrievalsForTask, postRatings } from "../be/memory/raters/llm";
41
+ import { contextBoundNoteThreshold, createContextBoundNote } from "../hooks/context-bound-note";
34
42
  import { getApiKey } from "../utils/api-key";
35
43
  import {
36
44
  CONTEXT_FORMULA,
37
45
  clampContextPercent,
38
46
  computeContextUsedUnified,
39
47
  } from "../utils/context-window";
48
+ import { selectCredential } from "../utils/credentials";
40
49
  import { isEnvFlagEnabled, isSessionSummarySkipped } from "../utils/env-flag";
41
50
  import { SessionErrorTracker } from "../utils/error-tracker";
42
51
  import { summarizeSession as runSummarize } from "../utils/internal-ai";
@@ -81,6 +90,32 @@ import type {
81
90
 
82
91
  type CodexConfig = Record<string, CodexConfigValue | undefined>;
83
92
 
93
+ /** `model_provider` id of the generated gateway block. */
94
+ const CODEX_GATEWAY_PROVIDER_ID = "swarm-gateway";
95
+
96
+ /**
97
+ * Config overrides that send Codex through the gateway route. Codex 0.164
98
+ * ignores `OPENAI_BASE_URL` and env keys for its built-in `openai` provider, so
99
+ * the gateway needs its own provider block. Verified on 0.164 against a local
100
+ * gateway: Codex sends `Authorization: Bearer $CODEX_GATEWAY_API_KEY` to
101
+ * `<base_url>/responses` and never the auth.json OAuth token. `wire_api`
102
+ * defaults to `responses` (`chat` is rejected); it is set to state intent.
103
+ */
104
+ export function codexGatewayConfig(route: ModelRoute): CodexConfig {
105
+ return {
106
+ model_provider: CODEX_GATEWAY_PROVIDER_ID,
107
+ model_providers: {
108
+ [CODEX_GATEWAY_PROVIDER_ID]: {
109
+ name: "Swarm gateway",
110
+ base_url: route.baseUrl,
111
+ env_key: CODEX_GATEWAY_API_KEY,
112
+ wire_api: "responses",
113
+ requires_openai_auth: false,
114
+ },
115
+ },
116
+ };
117
+ }
118
+
84
119
  /** Plugin id the worker image installs (`codex plugin add context-mode@context-mode`). */
85
120
  const CONTEXT_MODE_CODEX_PLUGIN = "context-mode@context-mode";
86
121
 
@@ -186,7 +221,10 @@ export type CodexThreadFactory = (options: {
186
221
  }) => Promise<CodexThread> | CodexThread;
187
222
 
188
223
  /**
189
- * Codex satisfies its credential requirement by ANY of:
224
+ * A gateway route (`CODEX_GATEWAY_BASE_URL` + `CODEX_GATEWAY_API_KEY`) wins
225
+ * and alone decides readiness: a gateway URL without its key is not ready,
226
+ * whatever OAuth or auth.json is present. Otherwise Codex satisfies its
227
+ * credential requirement by ANY of:
190
228
  * 1. `~/.codex/auth.json` already exists on disk (the canonical state once
191
229
  * `codex login` has run).
192
230
  * 2. `OPENAI_API_KEY` is set — the entrypoint will run
@@ -203,6 +241,13 @@ export function checkCodexCredentials(
203
241
  env: Record<string, string | undefined>,
204
242
  opts: CredCheckOptions = {},
205
243
  ): CredStatus {
244
+ const gateway = deriveDefaultRoute("codex", env);
245
+ if (gateway) {
246
+ const status = routeCredentialStatus(gateway, env);
247
+ return status.ready
248
+ ? { ready: true, missing: [], satisfiedBy: "env", hint: status.hint }
249
+ : { ready: false, missing: status.missing, hint: status.hint };
250
+ }
206
251
  const homeDir = opts.homeDir ?? env.HOME ?? "/root";
207
252
  const existsSync = opts.fs?.existsSync ?? nodeExistsSync;
208
253
  const authFile = `${homeDir}/.codex/auth.json`;
@@ -995,6 +1040,8 @@ export class CodexSession implements ProviderSession {
995
1040
  * summary path which materially relies on this ordering for testability.
996
1041
  */
997
1042
  private pendingResult: ProviderResult | null = null;
1043
+ /** Fed each main-thread request's prompt size; steers the one-time context-bound note. */
1044
+ private readonly observeContextBound: (contextTokens: number | undefined) => Promise<void>;
998
1045
 
999
1046
  constructor(
1000
1047
  thread: CodexThread,
@@ -1015,6 +1062,13 @@ export class CodexSession implements ProviderSession {
1015
1062
  skillsDir ?? process.env.CODEX_SKILLS_DIR ?? join(codexUserHome(), ".codex", "skills");
1016
1063
  this.summarizeDeps = summarizeDeps;
1017
1064
  this.logFileHandle = Bun.file(config.logFile).writer();
1065
+ this.observeContextBound = createContextBoundNote({
1066
+ threshold: contextBoundNoteThreshold(config.env ?? process.env),
1067
+ model: () => this.resolvedModel,
1068
+ // `turn/steer` only joins the active turn, so a late note never starts one.
1069
+ canDeliver: () => this.acceptsSteering && !this.settled && !this.aborted,
1070
+ inject: (note) => this.deliverSteering({ mode: "steer", text: note }),
1071
+ });
1018
1072
  // Mirrors the resolution `buildCodexConfig` already applied when
1019
1073
  // constructing `mergedConfig` — cheap, pure, and keeps the applied-level
1020
1074
  // feedback co-located with `settle()` rather than threading an extra
@@ -1425,6 +1479,8 @@ export class CodexSession implements ProviderSession {
1425
1479
  break;
1426
1480
  case "context.updated":
1427
1481
  this.emitContextUsage(event.usage, event.contextWindow ?? this.contextWindow);
1482
+ // Codex input tokens already include the cached read and write subsets.
1483
+ void this.observeContextBound(event.usage.input_tokens);
1428
1484
  break;
1429
1485
  case "error": {
1430
1486
  const { message } = this.formatTerminalError(event.message, event.codexErrorInfo);
@@ -1953,14 +2009,21 @@ export async function createInProcessCodexSession(
1953
2009
  preSessionEvents.push(event);
1954
2010
  };
1955
2011
 
1956
- const mergedConfig = await buildCodexConfig(config, resolvedModel, bufferedEmit);
2012
+ const routeEnv = config.env ?? process.env;
2013
+ const gateway = deriveDefaultRoute("codex", routeEnv);
2014
+ const mergedConfig = {
2015
+ ...(await buildCodexConfig(config, resolvedModel, bufferedEmit)),
2016
+ ...(gateway ? codexGatewayConfig(gateway) : {}),
2017
+ };
1957
2018
 
1958
2019
  // Auth resolution. `codex_oauth` (in the swarm config store) wins over
1959
2020
  // `OPENAI_API_KEY` so users can keep an OpenAI key set for embeddings
1960
2021
  // without it shadowing their ChatGPT login. The entrypoint already runs
1961
2022
  // this same precedence at boot — this block handles local dev (where
1962
2023
  // the entrypoint didn't run) and any case where auth.json is stale.
1963
- const authMode = await resolveCodexAuthMode(config, bufferedEmit);
2024
+ // A gateway route skips it: no OAuth is restored for a session whose traffic
2025
+ // goes to the gateway.
2026
+ const authMode = gateway ? null : await resolveCodexAuthMode(config, bufferedEmit);
1964
2027
 
1965
2028
  // The app-server receives an explicit environment. Construct a
1966
2029
  // minimal env explicitly so the spawned Codex CLI can find its binary
@@ -1987,7 +2050,14 @@ export async function createInProcessCodexSession(
1987
2050
  ...buildOtelTraceparentEnv(config.env ?? process.env),
1988
2051
  SWARM_CODEX_APP_SERVER: "1",
1989
2052
  };
1990
- if (authMode === "chatgpt") delete env.OPENAI_API_KEY;
2053
+ if (gateway) {
2054
+ for (const key of routeUnsetEnv("codex", gateway)) delete env[key];
2055
+ const gatewayKey = routeEnv[CODEX_GATEWAY_API_KEY];
2056
+ if (gatewayKey) env[CODEX_GATEWAY_API_KEY] = gatewayKey;
2057
+ } else if (authMode === "chatgpt") delete env.OPENAI_API_KEY;
2058
+ else if (env.OPENAI_API_KEY) {
2059
+ env.OPENAI_API_KEY = selectCredential(env.OPENAI_API_KEY, undefined, "OPENAI_API_KEY").selected;
2060
+ }
1991
2061
 
1992
2062
  const codexPathOverride = process.env.CODEX_PATH_OVERRIDE;
1993
2063
 
@@ -2139,6 +2209,11 @@ class CodexSubprocessSession implements ProviderSession {
2139
2209
  };
2140
2210
 
2141
2211
  const apiKey = getApiKey();
2212
+ const providerKeys: Record<string, string> = {};
2213
+ for (const keyVar of ["OPENAI_API_KEY", "OPENROUTER_API_KEY", "ANTHROPIC_API_KEY"]) {
2214
+ const value = config.env?.[keyVar] ?? process.env[keyVar];
2215
+ if (value) providerKeys[keyVar] = selectCredential(value, undefined, keyVar).selected;
2216
+ }
2142
2217
 
2143
2218
  this.proc = registerProcessGroup(
2144
2219
  Bun.spawn(argv, {
@@ -2156,13 +2231,7 @@ class CodexSubprocessSession implements ProviderSession {
2156
2231
  ...(process.env.MCP_BASE_URL ? { MCP_BASE_URL: process.env.MCP_BASE_URL } : {}),
2157
2232
  ...(apiKey ? { AGENT_SWARM_API_KEY: apiKey, API_KEY: apiKey } : {}),
2158
2233
  // Embedding / summarization paths read these:
2159
- ...(process.env.OPENAI_API_KEY ? { OPENAI_API_KEY: process.env.OPENAI_API_KEY } : {}),
2160
- ...(process.env.OPENROUTER_API_KEY
2161
- ? { OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY }
2162
- : {}),
2163
- ...(process.env.ANTHROPIC_API_KEY
2164
- ? { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY }
2165
- : {}),
2234
+ ...providerKeys,
2166
2235
  ...(process.env.CODEX_PATH_OVERRIDE
2167
2236
  ? { CODEX_PATH_OVERRIDE: process.env.CODEX_PATH_OVERRIDE }
2168
2237
  : {}),
@@ -2463,6 +2532,9 @@ export class CodexAdapter implements ProviderAdapter {
2463
2532
  }
2464
2533
 
2465
2534
  async createSession(config: ProviderSessionConfig): Promise<ProviderSession> {
2535
+ if (deriveDefaultRoute("codex", config.env ?? process.env)) {
2536
+ console.log("\x1b[2m[codex]\x1b[0m Using credential: gateway");
2537
+ }
2466
2538
  if (this.bypassSubprocess) {
2467
2539
  return createInProcessCodexSession(config, {
2468
2540
  skillsDir: this.skillsDir,
@@ -21,6 +21,7 @@ import {
21
21
  providerKeyVars,
22
22
  } from "@desplega/model-catalog";
23
23
  import type { OpenCode, OpenCodeEvent } from "@opencode/sdk";
24
+ import { contextBoundNoteThreshold, createContextBoundNote } from "../hooks/context-bound-note";
24
25
  import {
25
26
  CONTEXT_FORMULA,
26
27
  clampContextPercent,
@@ -534,6 +535,8 @@ export class OpencodeSession implements ProviderSession {
534
535
  // Assistant text by message, in part order; the runner keeps the last emit.
535
536
  private textByMessage = new Map<string, string[]>();
536
537
  private transcript: string[] = [];
538
+ /** Fed each step's prompt size; steers the one-time context-bound note. */
539
+ private readonly observeContextBound: (contextTokens: number | undefined) => Promise<void>;
537
540
 
538
541
  constructor(
539
542
  private readonly host: OpenCode.Interface,
@@ -547,6 +550,20 @@ export class OpencodeSession implements ProviderSession {
547
550
  this.completionPromise = new Promise<ProviderResult>((resolve) => {
548
551
  this.completionResolve = resolve;
549
552
  });
553
+ this.observeContextBound = createContextBoundNote({
554
+ threshold: contextBoundNoteThreshold(swarm.env ?? process.env),
555
+ model: () => this.model,
556
+ canDeliver: () => !this.completed && !this.aborted,
557
+ // A steer is promoted at the next step boundary mid-work; `resume: false`
558
+ // leaves an idle session idle. Same route as OpenCode's own Plan reminder.
559
+ inject: (note) =>
560
+ host.sessions.synthetic({
561
+ sessionID: _sessionId,
562
+ text: note,
563
+ delivery: "steer",
564
+ resume: false,
565
+ }),
566
+ });
550
567
  }
551
568
 
552
569
  get sessionId(): string {
@@ -685,6 +702,7 @@ export class OpencodeSession implements ProviderSession {
685
702
  // Per-step usage drives the dashboard's context gauge.
686
703
  const t = ev.data.tokens;
687
704
  const contextUsed = t.input + t.cache.read + t.cache.write + t.output;
705
+ void this.observeContextBound(t.input + t.cache.read + t.cache.write);
688
706
  const contextTotal = getContextWindowSize(this.model || "default");
689
707
  if (contextTotal > 0 && contextUsed > 0) {
690
708
  this.emit({
@@ -61,6 +61,7 @@ import { type TSchema, Type } from "typebox";
61
61
  import "./pi-codemode-runtime";
62
62
  // Registers the reprompt template in the code registry (the runner loads it too).
63
63
  import "../commands/templates";
64
+ import { contextBoundNoteThreshold, createContextBoundNote } from "../hooks/context-bound-note";
64
65
  import { resolveTemplateAsync } from "../prompts/resolver";
65
66
  import { CORE_TOOLS } from "../tools/tool-config";
66
67
  import { classifyAwsSdkError } from "../utils/aws-error-classifier";
@@ -907,6 +908,17 @@ function safeLabel(value: unknown): string {
907
908
  return typeof value === "string" && /^[A-Za-z0-9_-]{1,32}$/.test(value) ? value : "other";
908
909
  }
909
910
 
911
+ /** Prompt size of the request behind an assistant message: input + cache read + cache write. */
912
+ function requestContextTokens(message: unknown): number | undefined {
913
+ const usage = (
914
+ message as { usage?: { input?: unknown; cacheRead?: unknown; cacheWrite?: unknown } }
915
+ )?.usage;
916
+ if (typeof usage?.input !== "number") return undefined;
917
+ const cacheRead = typeof usage.cacheRead === "number" ? usage.cacheRead : 0;
918
+ const cacheWrite = typeof usage.cacheWrite === "number" ? usage.cacheWrite : 0;
919
+ return usage.input + cacheRead + cacheWrite;
920
+ }
921
+
910
922
  function describeAssistantTurn(message: unknown): AssistantTurnShape {
911
923
  const msg = (message ?? {}) as {
912
924
  content?: unknown;
@@ -1008,6 +1020,14 @@ export class PiMonoSession implements ProviderSession {
1008
1020
  private abortRequested = false;
1009
1021
  /** Removes this session's own models.json once the session ends. */
1010
1022
  private disposeModels: () => Promise<void>;
1023
+ /** Fed each assistant request's prompt size; steers the one-time context-bound note. */
1024
+ private readonly observeContextBound: (contextTokens: number | undefined) => Promise<void>;
1025
+ /**
1026
+ * From a tool-call reply until its turn ends. Pi reads steering after the
1027
+ * turn's tool results; a steer queued later waits for the next reply, and
1028
+ * when that is the final one it starts another turn.
1029
+ */
1030
+ private toolTurnOpen = false;
1011
1031
 
1012
1032
  constructor(
1013
1033
  agentSession: AgentSession,
@@ -1024,6 +1044,12 @@ export class PiMonoSession implements ProviderSession {
1024
1044
  this.logFileHandle = Bun.file(config.logFile).writer();
1025
1045
  this._sessionId = agentSession.sessionId;
1026
1046
  this.sessionStartedAt = Date.now();
1047
+ this.observeContextBound = createContextBoundNote({
1048
+ threshold: contextBoundNoteThreshold(config.env ?? process.env),
1049
+ model: () => this.reportedModel(),
1050
+ canDeliver: () => this.toolTurnOpen && !this.sessionEnded && !this.abortRequested,
1051
+ inject: (note) => this.deliverSteering({ mode: "steer", text: note }),
1052
+ });
1027
1053
 
1028
1054
  // Emit session_init immediately
1029
1055
  const piVersion = readPkgVersion("@earendil-works/pi-coding-agent");
@@ -1098,6 +1124,12 @@ export class PiMonoSession implements ProviderSession {
1098
1124
  }
1099
1125
  // A successful assistant turn means any prior error has recovered.
1100
1126
  this.terminalError = null;
1127
+ // Only on tool-call turns: a steer after the final reply would start
1128
+ // another turn.
1129
+ if (turn.hasToolCall) {
1130
+ this.toolTurnOpen = true;
1131
+ void this.observeContextBound(requestContextTokens(event.message));
1132
+ }
1101
1133
  if (!turn.hasText && !turn.hasToolCall) {
1102
1134
  // Nothing else records these turns: no text and no tool call leaves
1103
1135
  // no trace in session logs. Block types only, never content.
@@ -1152,6 +1184,9 @@ export class PiMonoSession implements ProviderSession {
1152
1184
  }
1153
1185
  break;
1154
1186
  }
1187
+ case "turn_end":
1188
+ this.toolTurnOpen = false;
1189
+ break;
1155
1190
  case "tool_execution_start": {
1156
1191
  const status = (event.args as { status?: unknown } | null)?.status;
1157
1192
  if (
@@ -177,7 +177,9 @@ export class DockerRuntimeProvider implements RuntimeProvider {
177
177
  try {
178
178
  await dockerApi(target, "POST", `/containers/${created.Id}/start`);
179
179
  } catch (err) {
180
- await dockerApi(target, "DELETE", `/containers/${created.Id}?force=true`).catch(() => {});
180
+ await dockerApi(target, "DELETE", `/containers/${created.Id}?force=true&v=true`).catch(
181
+ () => {},
182
+ );
181
183
  throw err;
182
184
  }
183
185
  return { kind: "docker", externalId: created.Id };
@@ -188,7 +190,9 @@ export class DockerRuntimeProvider implements RuntimeProvider {
188
190
 
189
191
  async terminate(target: RuntimeTargetConfig, handle: RuntimeHandle): Promise<void> {
190
192
  try {
191
- await dockerApi(target, "DELETE", `/containers/${handle.externalId}?force=true`);
193
+ // v=true also removes the container's anonymous volumes (the worker image declares
194
+ // VOLUME /logs); named volumes are never removed by it.
195
+ await dockerApi(target, "DELETE", `/containers/${handle.externalId}?force=true&v=true`);
192
196
  } catch (err) {
193
197
  if (!(err instanceof DockerApiError) || err.statusCode !== 404) throw err;
194
198
  }
@@ -91,8 +91,9 @@ async function executeScheduleScript(schedule: ScheduledTask): Promise<void> {
91
91
  agentId,
92
92
  scriptName: schedule.scriptName,
93
93
  // A pointer, not a copy: script schedules fire as often as every minute and
94
- // script_runs has no retention sweep, so copying the source on every fire
95
- // would grow the table by tens of MB a day. script_versions keeps the text.
94
+ // script_runs retention (SCRIPT_RUNS_RETENTION_DAYS) is off by default, so
95
+ // copying the source on every fire would grow the table by tens of MB a
96
+ // day. script_versions keeps the text.
96
97
  source: `// Scheduled run of '${script.name}' v${script.version} (global, contentHash ${script.contentHash}) by schedule '${schedule.name}'.\n// Full source: script_versions row for this version.\n`,
97
98
  args,
98
99
  startedAt: new Date().toISOString(),
@@ -319,7 +319,13 @@ function bridgeRequestFor(name: string, args: unknown): BridgeRequest | null {
319
319
  case "workflow_getRun": {
320
320
  const id = typeof body.id === "string" ? body.id : undefined;
321
321
  if (!id) throw new Error("workflow_getRun requires string `id`");
322
- return { method: "GET", path: `/api/workflow-runs/${encodeURIComponent(id)}` };
322
+ if (body.fields !== undefined && body.fields !== "full" && body.fields !== "slim") {
323
+ throw new Error('workflow_getRun `fields` must be "full" or "slim"');
324
+ }
325
+ return {
326
+ method: "GET",
327
+ path: appendQuery(`/api/workflow-runs/${encodeURIComponent(id)}`, { fields: body.fields }),
328
+ };
323
329
  }
324
330
  case "workflow_delete": {
325
331
  const id = typeof body.id === "string" ? body.id : undefined;
@@ -378,7 +378,7 @@ declare module "swarm-sdk" {
378
378
  limit?: number;
379
379
  offset?: number;
380
380
  }): Promise<unknown>;
381
- workflow_getRun(args: { id: string }): Promise<unknown>;
381
+ workflow_getRun(args: { id: string; fields?: "full" | "slim" }): Promise<unknown>;
382
382
  // --- prompt templates ---
383
383
  prompt_list(args?: {
384
384
  eventType?: string;
@@ -9,16 +9,16 @@ declare module "swarm-extension" {
9
9
  | "schedule"
10
10
  | "slack"
11
11
  | "agentmail"
12
- | "mcp"
13
- | "api"
14
- | "ui"
15
12
  | "github"
16
13
  | "gitlab"
17
14
  | "azure-devops"
18
15
  | "bitbucket"
19
- | "system"
20
16
  | "linear"
21
17
  | "jira"
18
+ | "mcp"
19
+ | "api"
20
+ | "ui"
21
+ | "system"
22
22
  | "comb"
23
23
  | "chat"
24
24
  | undefined;
@@ -127,16 +127,16 @@ declare module "swarm-extension" {
127
127
  | "schedule"
128
128
  | "slack"
129
129
  | "agentmail"
130
- | "mcp"
131
- | "api"
132
- | "ui"
133
130
  | "github"
134
131
  | "gitlab"
135
132
  | "azure-devops"
136
133
  | "bitbucket"
137
- | "system"
138
134
  | "linear"
139
135
  | "jira"
136
+ | "mcp"
137
+ | "api"
138
+ | "ui"
139
+ | "system"
140
140
  | "comb"
141
141
  | "chat";
142
142
  tags: string[];
@@ -401,6 +401,45 @@ declare module "swarm-extension" {
401
401
  description: string;
402
402
  origin: TaskCreateOrigin;
403
403
  requestInfo?: RequestInfo;
404
+ /** Slack ingress metadata; not persisted or writable through modify. */
405
+ slack?: {
406
+ trigger: "mention" | "thread-buffer";
407
+ messages: {
408
+ ts: string;
409
+ userId: string;
410
+ text: string;
411
+ }[];
412
+ };
413
+ }
414
+ /** A pre.task.create hook blocked a task before it was created. */
415
+ export interface TaskBlockedEvent {
416
+ /** The extension that blocked, or whose model/agent rewrite was rejected. */
417
+ extension: {
418
+ id: string;
419
+ name: string;
420
+ };
421
+ /** The scrubbed block reason. */
422
+ reason: string;
423
+ origin: TaskCreateOrigin;
424
+ source?: AgentTask["source"];
425
+ requester: {
426
+ userId?: string;
427
+ agentId?: string;
428
+ slackUserId?: string;
429
+ };
430
+ context?: {
431
+ contextKey?: string;
432
+ scheduleId?: string;
433
+ workflowRunId?: string;
434
+ workflowRunStepId?: string;
435
+ parentTaskId?: string;
436
+ vcsRepo?: string;
437
+ vcsNumber?: number;
438
+ slackChannelId?: string;
439
+ slackThreadTs?: string;
440
+ };
441
+ /** The original task description, scrubbed and cut to 500 characters. */
442
+ description: string;
404
443
  }
405
444
  export type TaskCreateModify = Partial<
406
445
  Pick<
@@ -704,6 +743,11 @@ declare module "swarm-extension" {
704
743
  modify: never;
705
744
  result: void;
706
745
  };
746
+ "post.task.blocked": {
747
+ event: TaskBlockedEvent;
748
+ modify: never;
749
+ result: void;
750
+ };
707
751
  "post.task.completed": {
708
752
  event: TaskCompletedEvent;
709
753
  modify: never;
@@ -360,7 +360,7 @@ declare module "swarm-sdk" {
360
360
  limit?: number;
361
361
  offset?: number;
362
362
  }): Promise<unknown>;
363
- workflow_getRun(args: { id: string }): Promise<unknown>;
363
+ workflow_getRun(args: { id: string; fields?: "full" | "slim" }): Promise<unknown>;
364
364
  // --- prompt templates ---
365
365
  prompt_list(args?: {
366
366
  eventType?: string;
@@ -213,6 +213,10 @@ export function createAssistant(): Assistant {
213
213
  buildEffectiveText(messageText, inbound.files, inbound.failed, inbound.notes),
214
214
  cachedBotUserId ?? undefined,
215
215
  );
216
+ const slack = {
217
+ trigger: "mention" as const,
218
+ messages: [{ ts: message.ts, userId, text: messageText }],
219
+ };
216
220
 
217
221
  if (workingAgent && workingAgent.status !== "offline") {
218
222
  // Otherwise, create a follow-up task for the working agent
@@ -236,6 +240,7 @@ export function createAssistant(): Assistant {
236
240
  contextKey: slackContextKey({ channelId, threadTs }),
237
241
  },
238
242
  inbound,
243
+ slack,
239
244
  );
240
245
  await ackSlackMessage(
241
246
  client,
@@ -285,6 +290,7 @@ export function createAssistant(): Assistant {
285
290
  contextKey: slackContextKey({ channelId, threadTs }),
286
291
  },
287
292
  inbound,
293
+ slack,
288
294
  );
289
295
  await ackSlackMessage(
290
296
  client,
@@ -318,6 +324,7 @@ export function createAssistant(): Assistant {
318
324
  contextKey: slackContextKey({ channelId, threadTs }),
319
325
  },
320
326
  inbound,
327
+ slack,
321
328
  );
322
329
  await ackSlackMessage(client, channelId, message.ts, reactionName("accepted"), "accepted");
323
330
  await notifySlackFileFailures(client, channelId, threadTs, unattached);
@@ -0,0 +1,74 @@
1
+ import type { WebClient } from "@slack/web-api";
2
+ import { rememberSessionLabel } from "../be/db/session-labels";
3
+ import { getSlackApp } from "./app";
4
+ import { callSlackWithRetry, slackErrorCode } from "./render-v2";
5
+
6
+ const LABEL_TTL_MS = 24 * 60 * 60 * 1000;
7
+ const FAILURE_TTL_MS = 10 * 60 * 1000;
8
+ const FAILURE_CACHE_MAX = 500;
9
+ const LOOKUP_QUEUE_MAX = 50;
10
+ const inFlight = new Set<string>();
11
+ const failedUntil = new Map<string, number>();
12
+ let pending = Promise.resolve();
13
+
14
+ async function channelLabel(client: WebClient, id: string): Promise<string> {
15
+ const result = await callSlackWithRetry(client, "conversations.info", {
16
+ channel: id.toUpperCase(),
17
+ });
18
+ const channel = result.channel as
19
+ | { name?: string; is_im?: boolean; is_mpim?: boolean; user?: string }
20
+ | undefined;
21
+ if (result.ok === false || !channel) throw new Error("Channel lookup failed");
22
+ if (channel.is_mpim) return "Group DM";
23
+ if (channel.is_im && channel.user) {
24
+ const result = await callSlackWithRetry(client, "users.info", { user: channel.user });
25
+ const user = result.user as
26
+ | { profile?: { display_name?: string; real_name?: string }; real_name?: string }
27
+ | undefined;
28
+ const name = user?.profile?.display_name || user?.real_name || user?.profile?.real_name;
29
+ if (result.ok !== false && name) return `DM · ${name}`;
30
+ } else if (channel.name) {
31
+ return `#${channel.name}`;
32
+ }
33
+ throw new Error("Channel label unavailable");
34
+ }
35
+
36
+ const CHANNEL_GROUP_KEY = /^shared\/slack\/([^/]+)\/$/;
37
+
38
+ /**
39
+ * Queue a Slack lookup for each channel group key (`shared/slack/<channel>/`)
40
+ * without delaying the tree response. Other keys are ignored.
41
+ */
42
+ export function warmSlackChannelLabels(groupKeys: string[]): void {
43
+ const client = getSlackApp()?.client;
44
+ if (!client) return;
45
+ for (const groupKey of groupKeys) {
46
+ const id = CHANNEL_GROUP_KEY.exec(groupKey)?.[1];
47
+ if (!id) continue;
48
+ if (inFlight.has(id) || (failedUntil.get(id) ?? 0) > Date.now()) continue;
49
+ failedUntil.delete(id);
50
+ if (inFlight.size >= LOOKUP_QUEUE_MAX) continue;
51
+ // Reserve failure capacity for queued work. Live failures must retain their TTL.
52
+ if (failedUntil.size + inFlight.size >= FAILURE_CACHE_MAX) {
53
+ const now = Date.now();
54
+ for (const [failedId, until] of failedUntil) {
55
+ if (until <= now) failedUntil.delete(failedId);
56
+ }
57
+ if (failedUntil.size + inFlight.size >= FAILURE_CACHE_MAX) continue;
58
+ }
59
+ inFlight.add(id);
60
+ pending = pending.then(async () => {
61
+ try {
62
+ const label = await channelLabel(client, id);
63
+ await rememberSessionLabel(groupKey, label, LABEL_TTL_MS);
64
+ } catch (error) {
65
+ failedUntil.set(id, Date.now() + FAILURE_TTL_MS);
66
+ console.warn(
67
+ `[Slack] Could not cache a session channel label: ${slackErrorCode(error) ?? "unknown error"}`,
68
+ );
69
+ } finally {
70
+ inFlight.delete(id);
71
+ }
72
+ });
73
+ }
74
+ }
@@ -679,6 +679,11 @@ export function registerMessageHandler(app: App): void {
679
679
  routingThreadContext,
680
680
  );
681
681
 
682
+ const slack = {
683
+ trigger: "mention" as const,
684
+ messages: [{ ts: msg.ts, userId: msg.user, text: msg.text ?? "" }],
685
+ };
686
+
682
687
  if (matches.length === 0) {
683
688
  if (!botMentioned && !isImplicitMention) return ignoreSlackInbound("not_addressed");
684
689
 
@@ -742,6 +747,7 @@ export function registerMessageHandler(app: App): void {
742
747
  contextKey: slackContextKey({ channelId: msg.channel, threadTs }),
743
748
  },
744
749
  inbound,
750
+ slack,
745
751
  );
746
752
  await ackSlackMessage(client, msg.channel, msg.ts, reactionName("accepted"), "accepted");
747
753
  await notifySlackFileFailures(client, msg.channel, threadTs, unattached);
@@ -864,6 +870,7 @@ export function registerMessageHandler(app: App): void {
864
870
  contextKey: slackContextKey({ channelId: msg.channel, threadTs }),
865
871
  },
866
872
  inbound,
873
+ slack,
867
874
  );
868
875
  const { task } = created;
869
876
  unattached.push(...created.unattached);
@@ -888,6 +895,7 @@ export function registerMessageHandler(app: App): void {
888
895
  contextKey: slackContextKey({ channelId: msg.channel, threadTs }),
889
896
  },
890
897
  inbound,
898
+ slack,
891
899
  );
892
900
  const { task } = created;
893
901
  unattached.push(...created.unattached);
@@ -22,6 +22,7 @@ import { join } from "node:path";
22
22
  import type { WebClient } from "@slack/web-api";
23
23
  import { getTaskAttachments, getTaskById, promoteDraftTask, refreshDraftTaskLease } from "../be/db";
24
24
  import { MAX_TASK_ATTACHMENT_BYTES, recordTaskAttachmentUpload } from "../be/task-attachment-store";
25
+ import type { TaskCreateEvent } from "../extensions/contract";
25
26
  import { providerPath } from "../fs/provider";
26
27
  import { getFileStorageProvider } from "../fs/registry";
27
28
  import { resolveTemplate } from "../prompts/resolver";
@@ -320,17 +321,19 @@ export async function createSlackTaskWithFiles(
320
321
  description: string,
321
322
  options: CreateTaskOptions,
322
323
  inbound: InboundSlackFiles,
324
+ slack?: TaskCreateEvent["slack"],
323
325
  ): Promise<{ task: AgentTask; unattached: SlackFileFailure[] }> {
324
326
  noteSlackInboundSideEffect("task_created");
327
+ const context = { origin: "slack" as const, slack };
325
328
  if (inbound.fetched.length === 0) {
326
- const task = await createTaskWithSiblingAwareness(description, options, { origin: "slack" });
329
+ const task = await createTaskWithSiblingAwareness(description, options, context);
327
330
  return { task, unattached: inbound.failed };
328
331
  }
329
332
 
330
333
  const task = await createTaskWithSiblingAwareness(
331
334
  description,
332
335
  { ...options, status: "draft" },
333
- { origin: "slack" },
336
+ context,
334
337
  );
335
338
  const unattached = [...inbound.failed];
336
339
  // Uploads run one after another and can outlast the abandoned-draft sweep's
@@ -313,6 +313,10 @@ async function slackFlush(
313
313
  contextKey: slackContextKey({ channelId, threadTs }),
314
314
  },
315
315
  inbound,
316
+ {
317
+ trigger: "thread-buffer",
318
+ messages: items.map(({ ts, userId, text }) => ({ ts, userId, text })),
319
+ },
316
320
  );
317
321
  } catch (error) {
318
322
  if (!(error instanceof TaskCreationBlockedError)) throw error;