@desplega.ai/agent-swarm 1.149.1 → 1.151.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 (538) hide show
  1. package/README.md +18 -3
  2. package/dist/{acp-adapter-vhkzfpjg.js → acp-adapter-ex8hzjfc.js} +5 -5
  3. package/dist/{actions-9d2694yk.js → actions-2bgpyhww.js} +39 -11
  4. package/dist/{app-fq2ggynt.js → app-1706zhr1.js} +7 -7
  5. package/dist/{artifact-gj9arwrr.js → artifact-td4q4dch.js} +1 -1
  6. package/dist/{assistant-edb6n9nn.js → assistant-bavke5p7.js} +44 -20
  7. package/dist/{azure-openai-responses-cqar64rc.js → azure-openai-responses-f38x2avq.js} +13 -13
  8. package/dist/{boot-reembed-msweabbp.js → boot-reembed-3cmrrmpa.js} +9 -9
  9. package/dist/{boot-reembed-q5wd3tha.js → boot-reembed-nafcmmmc.js} +14 -16
  10. package/dist/{boot-scrub-logs-3g1az933.js → boot-scrub-logs-cd6a2sq0.js} +5 -5
  11. package/dist/{claude-adapter-v3rnzsdc.js → claude-adapter-wfp2gzvz.js} +15 -16
  12. package/dist/{claude-managed-adapter-qvsmh6mv.js → claude-managed-adapter-dsmkcrjy.js} +3 -3
  13. package/dist/{claude-sdk-session-qxrpsna1.js → claude-sdk-session-bht9jjak.js} +15 -16
  14. package/dist/{cli-5m0qmvdp.js → cli-0qb1z6s2.js} +1 -1
  15. package/dist/{cli-9d8ntetj.js → cli-2658f6at.js} +1 -1
  16. package/dist/{cli-7evafqvy.js → cli-2tbvf1z1.js} +85 -69
  17. package/dist/{cli-k351hv5k.js → cli-3v9xhjcn.js} +24 -12
  18. package/dist/{cli-1ebpar1q.js → cli-4wppfkcm.js} +350 -20
  19. package/dist/{cli-4260r1tg.js → cli-5jd2z7av.js} +97 -84
  20. package/dist/{cli-cv7s5zdq.js → cli-5tpgq60f.js} +9865 -715
  21. package/dist/{cli-6skz3rk8.js → cli-7tgcstn2.js} +9 -4
  22. package/dist/{cli-01g3x1ca.js → cli-9004vfdf.js} +108 -56
  23. package/dist/{cli-1c94wvcw.js → cli-a1z3h107.js} +124 -99
  24. package/dist/{cli-9br8ypfv.js → cli-czjtgfys.js} +2 -2
  25. package/dist/{cli-5my3bjsd.js → cli-d3p82erb.js} +7 -7
  26. package/dist/{cli-e29gmdmw.js → cli-dv6cdjs2.js} +2 -1
  27. package/dist/{cli-wby1xnnb.js → cli-f1wky6zp.js} +1 -1
  28. package/dist/{cli-zvz62chv.js → cli-fm4ytsdk.js} +4 -4
  29. package/dist/{cli-tg8qj8ee.js → cli-frcta2w4.js} +6 -6
  30. package/dist/{cli-7xx10h0a.js → cli-g69fdc9g.js} +2 -2
  31. package/dist/{cli-cmp8g3sw.js → cli-gbfwcd3k.js} +44 -9
  32. package/dist/{handlers-nsg0bcye.js → cli-gjyvhmmh.js} +97 -45
  33. package/dist/{cli-08b07b4r.js → cli-ht5rs63n.js} +1 -1
  34. package/dist/{cli-jecc01cb.js → cli-j4nv2wqy.js} +2 -4
  35. package/dist/{cli-cd60fv6j.js → cli-j9xczhaq.js} +1 -1
  36. package/dist/cli-pf9q3696.js +172 -0
  37. package/dist/{cli-xm87hzbw.js → cli-pgj4a5fb.js} +1 -1
  38. package/dist/{cli-jezx9b9c.js → cli-ppm4975n.js} +9 -3
  39. package/dist/{cli-3z634pqe.js → cli-q88st9aj.js} +2 -2
  40. package/dist/{cli-3sysyjve.js → cli-r2q6pevn.js} +142 -49
  41. package/dist/{cli-07jd68a1.js → cli-re8z5p8p.js} +1 -1
  42. package/dist/{cli-qgd4sqz5.js → cli-s14agm5s.js} +121 -14
  43. package/dist/{cli-svbrpf5q.js → cli-sjaae6ff.js} +9872 -424
  44. package/dist/{cli-73gq0zxn.js → cli-vwm416m6.js} +13 -13
  45. package/dist/{cli-4wtq5jjv.js → cli-wbyk465z.js} +1 -1
  46. package/dist/{cli-3apbzgyk.js → cli-z61ykvam.js} +19 -14
  47. package/dist/{cli-e06a8a7y.js → cli-zkxymas5.js} +4 -3
  48. package/dist/cli.js +26 -25
  49. package/dist/{codex-adapter-jyw7a91z.js → codex-adapter-jjjk8w4m.js} +8 -9
  50. package/dist/{codex-hook-hfrt9shr.js → codex-hook-8jexc34x.js} +3 -3
  51. package/dist/{codex-session-runner-bmngj0s5.js → codex-session-runner-nd6ayxkx.js} +8 -9
  52. package/dist/{commands-n755g391.js → commands-9v4psyq1.js} +5 -5
  53. package/dist/{db-s1cgh5gv.js → db-wnt58tpe.js} +5 -5
  54. package/dist/dispatcher-05jh1aqk.js +74 -0
  55. package/dist/{e2b-p04vmtx0.js → e2b-413b8t4v.js} +1 -1
  56. package/dist/{google-generative-ai-tsskx7te.js → google-generative-ai-ndjmj5fa.js} +7 -7
  57. package/dist/{google-vertex-ppw8ydpa.js → google-vertex-fr665tqs.js} +10 -10
  58. package/dist/handlers-tw90pkh5.js +65 -0
  59. package/dist/{hook-jf0xe8zh.js → hook-123e2scb.js} +11 -12
  60. package/dist/{hook-h9cqrm8q.js → hook-sxe3r349.js} +11 -12
  61. package/dist/{http-nc6pydw5.js → http-ecsar8ez.js} +1442 -581
  62. package/dist/{index-ezxkds2n.js → index-16js521z.js} +335 -107
  63. package/dist/{index-769nc012.js → index-hw26vj40.js} +25 -19
  64. package/dist/{index-0wdy6gwc.js → index-nnrbpcjp.js} +25 -17
  65. package/dist/index-ymn7em0p.js +25 -0
  66. package/dist/{index-x1zcj09z.js → index-zj518gf9.js} +26 -20
  67. package/dist/{keepalive-nesm3xb6.js → keepalive-asqtcm4y.js} +7 -7
  68. package/dist/{lead-bwy6t2za.js → lead-xbfr7fvt.js} +40 -48
  69. package/dist/{maintenance-8w393zf0.js → maintenance-qma55qvw.js} +9 -9
  70. package/dist/{oauth-refresh-sweep-sh8xjy08.js → oauth-refresh-sweep-3s04c6qw.js} +6 -6
  71. package/dist/{onboard-25ps29h1.js → onboard-drskd386.js} +13 -4
  72. package/dist/{openai-codex-responses-g7w48y30.js → openai-codex-responses-wy6a43wd.js} +13 -13
  73. package/dist/{openai-responses-nmeyptke.js → openai-responses-6xwf7bzg.js} +19 -19
  74. package/dist/{opencode-adapter-dce4wskd.js → opencode-adapter-28axqa09.js} +4 -4
  75. package/dist/{otel-impl-b93gxnz5.js → otel-impl-943c96er.js} +1 -1
  76. package/dist/{pi-mono-adapter-zvshk5cj.js → pi-mono-adapter-jcqt71cg.js} +17 -19
  77. package/dist/{pricing-refresh-5v56x7gb.js → pricing-refresh-mx51az05.js} +7 -7
  78. package/dist/{rbac-roles-x55cnezn.js → rbac-roles-78baecqd.js} +13 -9
  79. package/dist/{rbac-roles-0p0s6pbj.js → rbac-roles-wcmrg6sv.js} +7 -7
  80. package/dist/{render-v2-hyd45w8b.js → render-v2-8zjntgf5.js} +7 -7
  81. package/dist/{seed-pricing-49gbgn96.js → seed-pricing-jqjs6cws.js} +6 -6
  82. package/dist/{setup-qvq64vzq.js → setup-4pks0vfk.js} +2 -2
  83. package/dist/work-objects-wangt792.js +165 -0
  84. package/dist/{worker-scj60cxg.js → worker-p2mr422x.js} +40 -48
  85. package/openapi.json +1365 -268
  86. package/package.json +4 -3
  87. package/src/agentmail/handlers.ts +95 -66
  88. package/src/be/budget-refusal-notify.ts +1 -0
  89. package/src/be/db/tasks/read.ts +3 -0
  90. package/src/be/db/tasks/write.ts +13 -1
  91. package/src/be/db-client.ts +5 -0
  92. package/src/be/db.ts +15 -5
  93. package/src/be/extensions/db.ts +461 -0
  94. package/src/be/extensions/validate.ts +107 -0
  95. package/src/be/memory/boot-reembed.ts +17 -12
  96. package/src/be/memory/index.ts +12 -0
  97. package/src/be/memory/providers/openai-embedding.ts +4 -0
  98. package/src/be/memory/types.ts +2 -0
  99. package/src/be/migrations/153_deferred_task_wait_members.sql +25 -0
  100. package/src/be/migrations/154_routing_source.sql +3 -0
  101. package/src/be/migrations/155_extensions.sql +66 -0
  102. package/src/be/migrations/156_inbox_item_state_read_at.sql +7 -0
  103. package/src/be/rbac-roles.ts +3 -2
  104. package/src/be/scripts/typecheck.ts +59 -41
  105. package/src/be/seed/schedules-seeder.ts +25 -2
  106. package/src/be/seed/workflows-seeder.ts +17 -0
  107. package/src/be/seed-scripts/catalog/delegate.ts +3 -1
  108. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  109. package/src/be/seed-skills/index.ts +7 -0
  110. package/src/be/steering.ts +1 -0
  111. package/src/be/swarm-config-guard.ts +3 -1
  112. package/src/commands/credential-refresh.ts +70 -0
  113. package/src/commands/onboard/compose-generator.ts +14 -2
  114. package/src/commands/onboard/env-generator.ts +7 -0
  115. package/src/commands/provider-credentials.ts +2 -25
  116. package/src/commands/runner.ts +24 -57
  117. package/src/extensions/apply-task-create.ts +135 -0
  118. package/src/extensions/bundle-path.ts +10 -0
  119. package/src/extensions/contract-runtime.ts +7 -0
  120. package/src/extensions/contract-types.generated.ts +4 -0
  121. package/src/extensions/contract.ts +270 -0
  122. package/src/extensions/ctx.ts +186 -0
  123. package/src/extensions/dispatcher.ts +479 -0
  124. package/src/extensions/identity.ts +44 -0
  125. package/src/extensions/lifecycle.ts +281 -0
  126. package/src/extensions/loader.ts +207 -0
  127. package/src/extensions/post-bridge.ts +161 -0
  128. package/src/github/handlers.ts +185 -140
  129. package/src/gitlab/handlers.ts +97 -77
  130. package/src/heartbeat/heartbeat.ts +155 -83
  131. package/src/http/agents.ts +1 -1
  132. package/src/http/all-routes.ts +1 -0
  133. package/src/http/approval-requests.ts +1 -0
  134. package/src/http/apps.ts +14 -9
  135. package/src/http/config.ts +4 -1
  136. package/src/http/core.ts +38 -2
  137. package/src/http/extensions.ts +547 -0
  138. package/src/http/inbox-state.ts +2 -0
  139. package/src/http/index.ts +41 -3
  140. package/src/http/mcp-bridge.ts +96 -62
  141. package/src/http/mcp.ts +1 -1
  142. package/src/http/script-runs.ts +5 -0
  143. package/src/http/status.ts +6 -4
  144. package/src/http/tasks.ts +51 -30
  145. package/src/integrations/kapso/inbound.ts +15 -10
  146. package/src/jira/sync.ts +26 -16
  147. package/src/linear/sync.ts +26 -16
  148. package/src/prompts/session-templates.ts +18 -13
  149. package/src/providers/claude-adapter.ts +21 -8
  150. package/src/rbac/admission.ts +1 -1
  151. package/src/rbac/elevated-agents.ts +24 -0
  152. package/src/rbac/legacy-policy.ts +35 -6
  153. package/src/rbac/permissions.ts +8 -0
  154. package/src/scheduler/deferred-task-waits.ts +138 -27
  155. package/src/scheduler/schedule-task.ts +54 -24
  156. package/src/scheduler/scheduler.ts +108 -125
  157. package/src/scripts-runtime/executors/native.ts +5 -1
  158. package/src/scripts-runtime/executors/types.ts +5 -0
  159. package/src/scripts-runtime/import-allowlist.ts +79 -19
  160. package/src/scripts-runtime/sdk-allowlist.ts +4 -0
  161. package/src/scripts-runtime/swarm-config.ts +8 -0
  162. package/src/scripts-runtime/swarm-sdk.ts +3 -0
  163. package/src/scripts-runtime/types/stdlib.d.ts +16 -3
  164. package/src/scripts-runtime/types/swarm-extension.d.ts +562 -0
  165. package/src/scripts-runtime/types/swarm-sdk.d.ts +16 -3
  166. package/src/server.ts +6 -0
  167. package/src/slack/HEURISTICS.md +3 -1
  168. package/src/slack/actions.ts +21 -16
  169. package/src/slack/app.ts +2 -0
  170. package/src/slack/assistant.ts +4 -12
  171. package/src/slack/blocks.ts +1 -1
  172. package/src/slack/handlers.ts +107 -19
  173. package/src/slack/inbound-files.ts +9 -5
  174. package/src/slack/render-v2.ts +188 -38
  175. package/src/slack/router.ts +13 -3
  176. package/src/slack/steering.ts +3 -3
  177. package/src/slack/thread-buffer.ts +66 -23
  178. package/src/slack/work-objects.test.ts +252 -0
  179. package/src/slack/work-objects.ts +118 -0
  180. package/src/tasks/errors.ts +9 -0
  181. package/src/tasks/sibling-awareness.ts +35 -1
  182. package/src/tasks/worker-follow-up.ts +67 -11
  183. package/src/tests/acp-adapter.test.ts +10 -10
  184. package/src/tests/agent-fs-live-url.test.ts +40 -0
  185. package/src/tests/agent-fs-provision-seeder.test.ts +12 -12
  186. package/src/tests/api-key-tracking.test.ts +18 -18
  187. package/src/tests/api-key.test.ts +4 -2
  188. package/src/tests/apps-rbac.test.ts +1 -1
  189. package/src/tests/apps-spike5.test.ts +1 -1
  190. package/src/tests/apps-sync-engine.test.ts +8 -8
  191. package/src/tests/apps-user-config.test.ts +1 -1
  192. package/src/tests/artifact-sdk.test.ts +8 -8
  193. package/src/tests/asset-key-mcp.test.ts +2 -0
  194. package/src/tests/automation-preflight-alert.test.ts +1 -1
  195. package/src/tests/base-prompt.test.ts +7 -6
  196. package/src/tests/budget-user-scope.test.ts +1 -1
  197. package/src/tests/budgets-routes.test.ts +1 -1
  198. package/src/tests/claude-adapter-binary.test.ts +22 -40
  199. package/src/tests/claude-adapter-otel.test.ts +13 -7
  200. package/src/tests/claude-adapter.test.ts +8 -6
  201. package/src/tests/claude-hook-credentials.test.ts +3 -3
  202. package/src/tests/claude-managed-adapter.test.ts +4 -4
  203. package/src/tests/claude-managed-setup.test.ts +3 -3
  204. package/src/tests/claude-sdk-lifecycle.test.ts +1 -1
  205. package/src/tests/claude-sdk-transport.test.ts +6 -6
  206. package/src/tests/claude-stop-hook.test.ts +8 -8
  207. package/src/tests/claude-worker-parity.test.ts +3 -3
  208. package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +5 -5
  209. package/src/tests/codex-adapter.test.ts +7 -5
  210. package/src/tests/codex-app-server.test.ts +2 -2
  211. package/src/tests/codex-login.test.ts +15 -15
  212. package/src/tests/codex-oauth-credential-info.test.ts +4 -4
  213. package/src/tests/codex-oauth-keep-warm.test.ts +4 -4
  214. package/src/tests/codex-oauth-refresh-lock.test.ts +1 -1
  215. package/src/tests/codex-oauth-storage.test.ts +7 -7
  216. package/src/tests/codex-pool.test.ts +1 -1
  217. package/src/tests/codex-steering-hook.test.ts +1 -1
  218. package/src/tests/codex-swarm-events.test.ts +1 -1
  219. package/src/tests/config-api-only-keys.test.ts +1 -1
  220. package/src/tests/config-session-auth.test.ts +2 -2
  221. package/src/tests/connection-embedded-auth.test.ts +20 -18
  222. package/src/tests/core-auth.test.ts +1 -1
  223. package/src/tests/create-page-tool.test.ts +3 -3
  224. package/src/tests/credential-refresh.test.ts +115 -0
  225. package/src/tests/credential-wait.test.ts +2 -2
  226. package/src/tests/credentials.test.ts +6 -4
  227. package/src/tests/db-queries-oauth.test.ts +40 -40
  228. package/src/tests/defer-task.test.ts +73 -8
  229. package/src/tests/deferred-task-wait-migration.test.ts +64 -0
  230. package/src/tests/deferred-task-wake.test.ts +349 -8
  231. package/src/tests/devin-adapter.test.ts +1 -1
  232. package/src/tests/devin-api.test.ts +1 -1
  233. package/src/tests/docs-index-footprint.test.js +3 -3
  234. package/src/tests/e2b-dispatch.test.ts +21 -19
  235. package/src/tests/e2e-claude-transport.test.ts +3 -3
  236. package/src/tests/e2e-redact.test.ts +6 -6
  237. package/src/tests/ensure-token.test.ts +68 -68
  238. package/src/tests/entrypoint-api-readiness.test.ts +1 -1
  239. package/src/tests/entrypoint-codex-oauth-seed.test.ts +13 -13
  240. package/src/tests/env-flag.test.ts +1 -0
  241. package/src/tests/events-http.test.ts +1 -1
  242. package/src/tests/extensions-db.test.ts +216 -0
  243. package/src/tests/extensions-dispatcher.test.ts +275 -0
  244. package/src/tests/extensions-example-heartbeat-remediate.test.ts +69 -0
  245. package/src/tests/extensions-example-mcp-install.test.ts +63 -0
  246. package/src/tests/extensions-example-no-exclamation-marks.test.ts +72 -0
  247. package/src/tests/extensions-example-rewrite-and-suppress.test.ts +70 -0
  248. package/src/tests/extensions-example-route-slack-channel.test.ts +217 -0
  249. package/src/tests/extensions-heartbeat.test.ts +291 -0
  250. package/src/tests/extensions-http.test.ts +431 -0
  251. package/src/tests/extensions-lead-equivalence.test.ts +49 -0
  252. package/src/tests/extensions-lifecycle.test.ts +209 -0
  253. package/src/tests/extensions-loader.test.ts +95 -0
  254. package/src/tests/extensions-mcp-tools.test.ts +245 -0
  255. package/src/tests/extensions-pre-task.test.ts +487 -0
  256. package/src/tests/extensions-run-context.test.ts +41 -0
  257. package/src/tests/extensions-tool-call.test.ts +288 -0
  258. package/src/tests/extensions-validate.test.ts +196 -0
  259. package/src/tests/fixtures/extensions/bad-import/hooks.ts +8 -0
  260. package/src/tests/fixtures/extensions/bad-import/manifest.json +9 -0
  261. package/src/tests/fixtures/extensions/bad-return-shape/hooks.ts +10 -0
  262. package/src/tests/fixtures/extensions/bad-return-shape/manifest.json +9 -0
  263. package/src/tests/fixtures/extensions/block-tasks-from-source/hooks.ts +22 -0
  264. package/src/tests/fixtures/extensions/block-tasks-from-source/manifest.json +9 -0
  265. package/src/tests/fixtures/extensions/ignore-channel/hooks.ts +22 -0
  266. package/src/tests/fixtures/extensions/ignore-channel/manifest.json +9 -0
  267. package/src/tests/fixtures/extensions/load.ts +10 -0
  268. package/src/tests/fixtures/extensions/minimal/hooks.ts +7 -0
  269. package/src/tests/fixtures/extensions/minimal/manifest.json +9 -0
  270. package/src/tests/fixtures/extensions/never-fail-long-tasks/hooks.ts +11 -0
  271. package/src/tests/fixtures/extensions/never-fail-long-tasks/manifest.json +9 -0
  272. package/src/tests/fixtures/extensions/no-exclamation-marks/hooks.ts +23 -0
  273. package/src/tests/fixtures/extensions/no-exclamation-marks/manifest.json +9 -0
  274. package/src/tests/fixtures/extensions/post-logger/hooks.ts +9 -0
  275. package/src/tests/fixtures/extensions/post-logger/manifest.json +9 -0
  276. package/src/tests/fixtures/extensions/priority-a/hooks.ts +7 -0
  277. package/src/tests/fixtures/extensions/priority-a/manifest.json +9 -0
  278. package/src/tests/fixtures/extensions/priority-b/hooks.ts +7 -0
  279. package/src/tests/fixtures/extensions/priority-b/manifest.json +9 -0
  280. package/src/tests/fixtures/extensions/record-only-on-tag/hooks.ts +16 -0
  281. package/src/tests/fixtures/extensions/record-only-on-tag/manifest.json +9 -0
  282. package/src/tests/fixtures/extensions/reserved-assets/hooks.ts +5 -0
  283. package/src/tests/fixtures/extensions/reserved-assets/manifest.json +10 -0
  284. package/src/tests/fixtures/extensions/rewrite-progress-prefix/hooks.ts +22 -0
  285. package/src/tests/fixtures/extensions/rewrite-progress-prefix/manifest.json +9 -0
  286. package/src/tests/fixtures/extensions/rewrite-task-priority/hooks.ts +7 -0
  287. package/src/tests/fixtures/extensions/rewrite-task-priority/manifest.json +9 -0
  288. package/src/tests/fixtures/extensions/route-channel-to-agent/hooks.ts +26 -0
  289. package/src/tests/fixtures/extensions/route-channel-to-agent/manifest.json +9 -0
  290. package/src/tests/fixtures/extensions/slow/hooks.ts +10 -0
  291. package/src/tests/fixtures/extensions/slow/manifest.json +9 -0
  292. package/src/tests/fixtures/extensions/suppress-lead-follow-up/hooks.ts +12 -0
  293. package/src/tests/fixtures/extensions/suppress-lead-follow-up/manifest.json +9 -0
  294. package/src/tests/fixtures/extensions/throws/hooks.ts +12 -0
  295. package/src/tests/fixtures/extensions/throws/manifest.json +9 -0
  296. package/src/tests/fixtures/extensions/worker-runtime/hooks.ts +5 -0
  297. package/src/tests/fixtures/extensions/worker-runtime/manifest.json +9 -0
  298. package/src/tests/fs-routes.test.ts +1 -1
  299. package/src/tests/gitlab-auth.test.ts +10 -10
  300. package/src/tests/harness-provider-resolution.test.ts +8 -5
  301. package/src/tests/heartbeat-reroute-decision.test.ts +1 -0
  302. package/src/tests/helper-credentials.test.ts +137 -0
  303. package/src/tests/http/context-routes.test.ts +2 -2
  304. package/src/tests/http-api-integration.test.ts +5 -2
  305. package/src/tests/http-users.test.ts +1 -1
  306. package/src/tests/identity-refresh.test.ts +2 -2
  307. package/src/tests/internal-ai/complete-structured.test.ts +2 -2
  308. package/src/tests/internal-ai/credentials.test.ts +19 -17
  309. package/src/tests/jira-metadata.test.ts +1 -1
  310. package/src/tests/jira-oauth.test.ts +5 -5
  311. package/src/tests/jira-sync.test.ts +1 -1
  312. package/src/tests/jira-webhook-lifecycle.test.ts +3 -3
  313. package/src/tests/jira-webhook.test.ts +2 -2
  314. package/src/tests/kapso-inbound.test.ts +2 -2
  315. package/src/tests/kv-http.test.ts +1 -1
  316. package/src/tests/kv-namespace-resolution.test.ts +1 -1
  317. package/src/tests/kv-page-proxy.test.ts +2 -2
  318. package/src/tests/launch-password-rejection.test.ts +2 -2
  319. package/src/tests/linear-webhook.test.ts +1 -1
  320. package/src/tests/mcp-oauth-dcr-reuse.test.ts +6 -6
  321. package/src/tests/mcp-oauth-ensure-token.test.ts +28 -22
  322. package/src/tests/mcp-oauth-manual-client.test.ts +16 -12
  323. package/src/tests/mcp-oauth-queries.test.ts +35 -32
  324. package/src/tests/mcp-oauth-resolve-secrets.test.ts +8 -8
  325. package/src/tests/mcp-oauth-wrapper.test.ts +20 -20
  326. package/src/tests/mcp-user-route.test.ts +1 -1
  327. package/src/tests/memory-edges.test.ts +1 -1
  328. package/src/tests/memory-edit-authorization.test.ts +32 -0
  329. package/src/tests/memory-rate-endpoint.test.ts +1 -1
  330. package/src/tests/memory-rate-tool.test.ts +2 -2
  331. package/src/tests/memory-rater-e2e.test.ts +1 -1
  332. package/src/tests/memory-rater-llm-summarizer.test.ts +3 -3
  333. package/src/tests/memory-rater-llm.test.ts +1 -1
  334. package/src/tests/model-control.test.ts +3 -0
  335. package/src/tests/multi-runtime-enabled-stats.test.ts +2 -2
  336. package/src/tests/multi-runtime-registration.test.ts +111 -17
  337. package/src/tests/oauth-access-token-tool.test.ts +18 -18
  338. package/src/tests/oauth-callback-flow.test.ts +8 -8
  339. package/src/tests/oauth-credential-bindings.test.ts +29 -27
  340. package/src/tests/oauth-keepalive.test.ts +10 -10
  341. package/src/tests/oauth-presets.test.ts +13 -5
  342. package/src/tests/oauth-refresh-failure.test.ts +9 -9
  343. package/src/tests/oauth-refresh-sweep.test.ts +20 -13
  344. package/src/tests/oauth-wrapper.test.ts +1 -1
  345. package/src/tests/onboard-compose.test.ts +47 -3
  346. package/src/tests/onboard-env.test.ts +26 -26
  347. package/src/tests/onboard-non-interactive.test.ts +4 -4
  348. package/src/tests/opencode-adapter.test.ts +1 -1
  349. package/src/tests/opencode-plugin.test.ts +2 -2
  350. package/src/tests/otel-session-cost-metrics.test.ts +1 -1
  351. package/src/tests/page-proxy-authed.test.ts +2 -2
  352. package/src/tests/page-proxy.test.ts +2 -2
  353. package/src/tests/page-session.test.ts +7 -7
  354. package/src/tests/page-viewer-auth.test.ts +2 -2
  355. package/src/tests/pages-authed-mode.test.ts +1 -1
  356. package/src/tests/pages-http.test.ts +1 -1
  357. package/src/tests/pages-password-mode.test.ts +1 -1
  358. package/src/tests/pages-public-authed-401.test.ts +1 -1
  359. package/src/tests/pages-public-html.test.ts +2 -2
  360. package/src/tests/pages-view-count.test.ts +1 -1
  361. package/src/tests/pi-mono-adapter.test.ts +10 -10
  362. package/src/tests/pi-mono-extension.test.ts +2 -2
  363. package/src/tests/preload.ts +5 -0
  364. package/src/tests/pricing-routes.test.ts +1 -1
  365. package/src/tests/profile-sync-warning.test.ts +8 -8
  366. package/src/tests/profile-sync.test.ts +8 -8
  367. package/src/tests/prompt-template-session.test.ts +36 -5
  368. package/src/tests/provider-process-group.test.ts +95 -4
  369. package/src/tests/providers/pi-cost.test.ts +1 -1
  370. package/src/tests/queue-stall-alarm.test.ts +1 -1
  371. package/src/tests/rbac-admission.test.ts +19 -4
  372. package/src/tests/rbac-charact-http.test.ts +1 -1
  373. package/src/tests/rbac-e2e-helpers.ts +1 -1
  374. package/src/tests/rbac-engine.test.ts +39 -0
  375. package/src/tests/realtime-room-auth.test.ts +1 -1
  376. package/src/tests/realtime-room-surfaces.test.ts +1 -1
  377. package/src/tests/reload-config.test.ts +45 -1
  378. package/src/tests/routing-affinity-inventory.test.ts +1 -1
  379. package/src/tests/routing-reason-contract.test.ts +76 -3
  380. package/src/tests/routing-reason-inventory.test.ts +11 -6
  381. package/src/tests/routing-source.test.ts +53 -0
  382. package/src/tests/runner-agent-fs-provision-retry.test.ts +1 -1
  383. package/src/tests/runner-context-preamble.test.ts +1 -1
  384. package/src/tests/runner-deployment-resume.test.ts +1 -1
  385. package/src/tests/runner-fallback-output.test.ts +1 -1
  386. package/src/tests/runner-identity-refresh.test.ts +1 -1
  387. package/src/tests/runner-skills-refresh.test.ts +1 -1
  388. package/src/tests/schedule-target-type.test.ts +3 -3
  389. package/src/tests/scheduled-tasks.test.ts +14 -5
  390. package/src/tests/scheduler-occurrence.test.ts +347 -0
  391. package/src/tests/script-apis-mcp.test.ts +1 -1
  392. package/src/tests/script-connections-http.test.ts +61 -58
  393. package/src/tests/script-connections-mcp.test.ts +1 -1
  394. package/src/tests/script-connections.test.ts +3 -1
  395. package/src/tests/script-runs-http.test.ts +15 -7
  396. package/src/tests/script-workflows-runtime-e2e.test.ts +1 -1
  397. package/src/tests/scripts-boot-reembed.test.ts +4 -0
  398. package/src/tests/scripts-embeddings.test.ts +4 -0
  399. package/src/tests/scripts-external-api.test.ts +1 -1
  400. package/src/tests/scripts-http.test.ts +1 -1
  401. package/src/tests/scripts-import-allowlist.test.ts +44 -1
  402. package/src/tests/scripts-mcp-e2e.test.ts +81 -1
  403. package/src/tests/scripts-only-gating.test.ts +4 -2
  404. package/src/tests/scripts-retention.test.ts +1 -1
  405. package/src/tests/scripts-runtime-identity.test.ts +2 -2
  406. package/src/tests/scripts-runtime-secret-egress.test.ts +1 -1
  407. package/src/tests/scripts-runtime.test.ts +6 -4
  408. package/src/tests/scripts-typecheck.test.ts +21 -0
  409. package/src/tests/sdk-allowlist.test.ts +1 -1
  410. package/src/tests/secret-scrubber.test.ts +38 -31
  411. package/src/tests/seed-registry.test.ts +46 -8
  412. package/src/tests/seed-schedules.test.ts +44 -9
  413. package/src/tests/seed-workflows.test.ts +21 -9
  414. package/src/tests/send-task-output-schema.test.ts +9 -1
  415. package/src/tests/send-task-requested-by.test.ts +1 -0
  416. package/src/tests/send-task-slack-routing-guard.test.ts +2 -0
  417. package/src/tests/session-costs-codex-recompute.test.ts +1 -1
  418. package/src/tests/session-costs-golden.test.ts +2 -2
  419. package/src/tests/session-costs-model-key-normalize.test.ts +2 -2
  420. package/src/tests/session-costs-recompute-all-providers.test.ts +1 -1
  421. package/src/tests/session-tokens.test.ts +1 -1
  422. package/src/tests/shutdown-error-scrubbing.test.ts +1 -1
  423. package/src/tests/slack-blocks.test.ts +12 -1
  424. package/src/tests/slack-file-tools-attachments.test.ts +1 -1
  425. package/src/tests/slack-inbound-files.test.ts +222 -86
  426. package/src/tests/slack-reaction-config.test.ts +5 -4
  427. package/src/tests/slack-read-boundaries.test.ts +1 -1
  428. package/src/tests/slack-render-v2.test.ts +196 -4
  429. package/src/tests/slack-router.test.ts +17 -2
  430. package/src/tests/slack-steering.test.ts +19 -0
  431. package/src/tests/status.test.ts +19 -7
  432. package/src/tests/steering-core.test.ts +1 -1
  433. package/src/tests/steering-enabled.test.ts +6 -8
  434. package/src/tests/steering-transport.test.ts +4 -4
  435. package/src/tests/store-progress-output-guidance.test.ts +34 -0
  436. package/src/tests/swarm-tool-result-gate.test.ts +11 -11
  437. package/src/tests/swarm-x-tool.test.ts +7 -5
  438. package/src/tests/sync-ai-toolbox-skills.test.ts +19 -0
  439. package/src/tests/task-supersede-resume.test.ts +1 -1
  440. package/src/tests/task-tool-preload-mcp.test.ts +4 -1
  441. package/src/tests/task-tools-ctx.test.ts +2 -0
  442. package/src/tests/tasks-requested-by-filter.test.ts +2 -2
  443. package/src/tests/telemetry-init.test.ts +11 -3
  444. package/src/tests/tool-annotations.test.ts +1 -1
  445. package/src/tests/tool-output-agent-id.test.ts +1 -0
  446. package/src/tests/tool-registrar-no-input.test.ts +42 -1
  447. package/src/tests/tracker-fold.test.ts +7 -7
  448. package/src/tests/tracker-tools.test.ts +3 -3
  449. package/src/tests/ui-followup-lead-delegation.test.ts +1 -0
  450. package/src/tests/unified-oauth-migration.test.ts +5 -5
  451. package/src/tests/user-token-rest-auth.test.ts +1 -1
  452. package/src/tests/user-token-routes.test.ts +1 -1
  453. package/src/tests/whoami-route.test.ts +1 -1
  454. package/src/tests/workflow-agent-task.test.ts +15 -13
  455. package/src/tests/workflow-ctx.test.ts +1 -1
  456. package/src/tests/workflow-e2e.test.ts +1 -1
  457. package/src/tests/workflow-engine-v2.test.ts +3 -1
  458. package/src/tests/workflow-executors.test.ts +2 -2
  459. package/src/tests/workflow-http-v2.test.ts +8 -6
  460. package/src/tests/workflow-input-redaction.test.ts +7 -5
  461. package/src/tests/workflow-integration-io.test.ts +2 -2
  462. package/src/tests/workflow-llm.test.ts +5 -5
  463. package/src/tests/workflow-registry.test.ts +1 -1
  464. package/src/tests/workflow-swarm-script.test.ts +1 -1
  465. package/src/tests/workflow-triggers-v2.test.ts +23 -21
  466. package/src/tests/x-composio.test.ts +9 -7
  467. package/src/tests/x402-client.test.ts +1 -1
  468. package/src/tests/x402-config.test.ts +28 -41
  469. package/src/tools/defer-task.ts +156 -24
  470. package/src/tools/extension-common.ts +84 -0
  471. package/src/tools/extension-install.ts +65 -0
  472. package/src/tools/extension-list.ts +66 -0
  473. package/src/tools/script-common.ts +43 -18
  474. package/src/tools/send-task.ts +87 -105
  475. package/src/tools/skills/skill-publish.ts +1 -0
  476. package/src/tools/store-progress.ts +1 -1
  477. package/src/tools/task-action.ts +43 -22
  478. package/src/tools/task-tool-ctx.ts +2 -0
  479. package/src/tools/templates.ts +2 -1
  480. package/src/tools/tool-config.ts +3 -1
  481. package/src/tools/utils.ts +140 -16
  482. package/src/types.ts +144 -0
  483. package/src/utils/constants.ts +7 -1
  484. package/src/utils/credentials.test.ts +16 -16
  485. package/src/utils/multi-runtime.ts +2 -2
  486. package/src/utils/steering-enabled.ts +4 -4
  487. package/src/workflows/executors/agent-task.ts +18 -3
  488. package/templates/ai-toolbox.manifest.json +7 -1
  489. package/templates/extensions/README.md +17 -0
  490. package/templates/extensions/notify-on-complete/README.md +11 -0
  491. package/templates/extensions/notify-on-complete/hooks.ts +61 -0
  492. package/templates/extensions/notify-on-complete/manifest.json +7 -0
  493. package/templates/extensions/require-ticket-ref/README.md +11 -0
  494. package/templates/extensions/require-ticket-ref/hooks.ts +30 -0
  495. package/templates/extensions/require-ticket-ref/manifest.json +7 -0
  496. package/templates/extensions/require-verification-note/README.md +11 -0
  497. package/templates/extensions/require-verification-note/hooks.ts +32 -0
  498. package/templates/extensions/require-verification-note/manifest.json +7 -0
  499. package/templates/schedules/daily-hn-briefing/config.json +1 -1
  500. package/templates/schedules/gtm-weekly-review/config.json +1 -1
  501. package/templates/schedules/weekly-code-health-reports/config.json +1 -1
  502. package/templates/schedules/weekly-dependabot-triage/config.json +1 -1
  503. package/templates/schedules/weekly-dora-metrics/config.json +1 -1
  504. package/templates/schedules/weekly-harness-upgrade-check/config.json +1 -1
  505. package/templates/skills/delegate-work/files/scripts/codex-exec.sh +1 -1
  506. package/templates/skills/scheduling/content.md +2 -0
  507. package/templates/skills/serply-search/SKILL.md +95 -0
  508. package/templates/skills/serply-search/config.json +13 -0
  509. package/templates/skills/serply-search/content.md +90 -0
  510. package/templates/skills/swarm-extensions/config.json +14 -0
  511. package/templates/skills/swarm-extensions/content.md +120 -0
  512. package/templates/skills/swarm-scripts/SKILL.md +2 -2
  513. package/templates/skills/swarm-scripts/content.md +2 -2
  514. package/templates/workflows/claude-code-changelog-watch/config.json +1 -1
  515. package/templates/workflows/gsc-topic-miner/config.json +1 -1
  516. package/templates/workflows/linear-drain-loop/config.json +1 -1
  517. package/templates/workflows/llm-safe-release-context/config.json +1 -1
  518. package/templates/workflows/ralph-loop/config.json +1 -1
  519. package/dist/cli-7fehefdf.js +0 -562
  520. package/dist/cli-ayvy5kde.js +0 -164
  521. package/dist/cli-ddfw1mc4.js +0 -4322
  522. package/dist/cli-hzqmgcm2.js +0 -3066
  523. package/dist/cli-j4d4f6hv.js +0 -338
  524. package/dist/cli-qgm2pfpq.js +0 -6377
  525. package/dist/cli-sdzq01t9.js +0 -823
  526. package/dist/cli-xvkmedgn.js +0 -332
  527. package/dist/cli-y3pq4v9g.js +0 -664
  528. package/dist/{anthropic-messages-vs2rabzp.js → anthropic-messages-2y8nktar.js} +19 -19
  529. package/dist/{claude-managed-setup-f5pmsyrp.js → claude-managed-setup-48dwg9bv.js} +3 -3
  530. package/dist/{cli-81r683bv.js → cli-ch38q5z4.js} +0 -0
  531. package/dist/{cli-6664w7y4.js → cli-kj76p1ez.js} +8 -8
  532. package/dist/{cli-c71mg51n.js → cli-r2fpe7cx.js} +5 -5
  533. package/dist/{cli-4xz3pmrq.js → cli-y6x0gqtg.js} +16 -16
  534. package/dist/{index-aa3sa6s8.js → index-2wsdsnmm.js} +1 -1
  535. package/dist/{mistral-conversations-4ryk7h83.js → mistral-conversations-qhr84ph3.js} +9 -9
  536. package/dist/{openai-completions-0bqmsajw.js → openai-completions-32rdwmze.js} +19 -19
  537. package/dist/{openrouter-images-qx18fyp8.js → openrouter-images-9f9ek4rw.js} +3 -3
  538. package/dist/{pi-messages-79jvzz1d.js → pi-messages-b1mbr6gp.js} +4 -4
@@ -77,7 +77,7 @@ type SlackThreadMessage = {
77
77
  };
78
78
 
79
79
  export function isSlackRenderV2Enabled(): boolean {
80
- return isEnvFlagEnabled("SLACK_RENDER_V2", false);
80
+ return isEnvFlagEnabled("SLACK_RENDER_V2", true);
81
81
  }
82
82
 
83
83
  export function isSlackDelegationEnabled(): boolean {
@@ -121,6 +121,88 @@ export async function callSlackWithRetry(
121
121
  }
122
122
  }
123
123
 
124
+ // Outcome delivery (chat.startStream, with its chat.postMessage fallback) can
125
+ // fail for reasons callSlackWithRetry does not retry (e.g. user_not_found).
126
+ // Without a bound, processSlackRenderV2's 3s tick re-attempts the same
127
+ // failing task forever. This tracks per-task failures in memory and applies
128
+ // capped exponential backoff plus a give-up ceiling so a persistently broken
129
+ // recipient stops hammering Slack instead of retrying at tick cadence.
130
+ const OUTCOME_DELIVERY_MAX_ATTEMPTS = 5;
131
+ const OUTCOME_DELIVERY_BASE_DELAY_MS = 30_000;
132
+ const OUTCOME_DELIVERY_MAX_DELAY_MS = 30 * 60_000;
133
+ const outcomeDeliveryFailures = new Map<string, { count: number; nextAttemptAt: number }>();
134
+
135
+ function outcomeDeliveryGate(taskId: string): boolean {
136
+ const state = outcomeDeliveryFailures.get(taskId);
137
+ if (!state) return true;
138
+ if (state.count >= OUTCOME_DELIVERY_MAX_ATTEMPTS) return false;
139
+ return Date.now() >= state.nextAttemptAt;
140
+ }
141
+
142
+ function noteOutcomeDeliverySuccess(taskId: string): void {
143
+ outcomeDeliveryFailures.delete(taskId);
144
+ }
145
+
146
+ async function noteOutcomeDeliveryFailure(task: AgentTask, error: unknown): Promise<void> {
147
+ const state = outcomeDeliveryFailures.get(task.id) ?? { count: 0, nextAttemptAt: 0 };
148
+ state.count += 1;
149
+ const delay = Math.min(
150
+ OUTCOME_DELIVERY_BASE_DELAY_MS * 2 ** (state.count - 1),
151
+ OUTCOME_DELIVERY_MAX_DELAY_MS,
152
+ );
153
+ state.nextAttemptAt = Date.now() + delay;
154
+ outcomeDeliveryFailures.set(task.id, state);
155
+ console.error(
156
+ `[Slack] Outcome delivery failed for task ${task.id} (attempt ${state.count}/${OUTCOME_DELIVERY_MAX_ATTEMPTS}):`,
157
+ error,
158
+ );
159
+ if (state.count >= OUTCOME_DELIVERY_MAX_ATTEMPTS) {
160
+ console.error(
161
+ `[Slack] Giving up on outcome delivery for task ${task.id} after ${state.count} attempts; surfacing failure and clearing the working indicator`,
162
+ );
163
+ await surfaceOutcomeDeliveryGiveUp(task, error);
164
+ }
165
+ }
166
+
167
+ /** Best-effort: post a visible failure notice and clear the "is working" status so a
168
+ * permanently broken delivery doesn't fail silently or leave the thread spinning. */
169
+ async function surfaceOutcomeDeliveryGiveUp(task: AgentTask, error: unknown): Promise<void> {
170
+ const app = getSlackApp();
171
+ if (!app || !task.slackChannelId || !task.slackThreadTs) return;
172
+ try {
173
+ await app.client.apiCall("chat.postMessage", {
174
+ channel: task.slackChannelId,
175
+ thread_ts: task.slackThreadTs,
176
+ text: `⚠️ Couldn't deliver this task's reply after ${OUTCOME_DELIVERY_MAX_ATTEMPTS} attempts (${
177
+ error instanceof Error ? error.message : String(error)
178
+ }). See task ${getTaskLink(task.id)} for the result.`,
179
+ });
180
+ } catch (postError) {
181
+ console.error(`[Slack] Give-up notice failed to post for task ${task.id}:`, postError);
182
+ }
183
+ await clearAssistantStatus(app.client, task.slackChannelId, task.slackThreadTs);
184
+ }
185
+
186
+ /** Clears the assistant "is working" indicator in DM channels. Best-effort: the
187
+ * call throws when the thread isn't an assistant thread, which is expected
188
+ * for non-DM channels and safe to ignore. */
189
+ async function clearAssistantStatus(
190
+ client: WebClient,
191
+ channelId: string,
192
+ threadTs: string,
193
+ ): Promise<void> {
194
+ if (!channelId.startsWith("D")) return;
195
+ try {
196
+ await client.apiCall("assistant.threads.setStatus", {
197
+ channel_id: channelId,
198
+ thread_ts: threadTs,
199
+ status: "",
200
+ });
201
+ } catch (error) {
202
+ console.warn(`[Slack] Failed to clear assistant status for ${channelId}/${threadTs}:`, error);
203
+ }
204
+ }
205
+
124
206
  function slackTreeStallMinutes(): number {
125
207
  return Number(process.env.SLACK_TREE_STALL_MIN) || 15;
126
208
  }
@@ -665,7 +747,11 @@ async function outcomeContent(task: AgentTask, slackReplySent: boolean): Promise
665
747
  if (task.status === "cancelled") {
666
748
  return `🚫 **Cancelled**\n\n${outcomeText(task.failureReason, "Task was cancelled.")}`;
667
749
  }
668
- if (slackReplySent && task.status === "completed") {
750
+ // A deferral's output is engine-authored, never posted by hand via
751
+ // slack-reply, so it must not collapse into the "agent completed" summary
752
+ // even when the agent also sent a slack-reply earlier in the same task.
753
+ const isDeferred = task.tags?.includes("deferred");
754
+ if (slackReplySent && task.status === "completed" && !isDeferred) {
669
755
  const agentName = task.agentId
670
756
  ? ((await getAgentById(task.agentId))?.name ?? "Agent")
671
757
  : "Agent";
@@ -697,12 +783,23 @@ export async function childOutcomeContent(
697
783
  if (task.status === "failed") {
698
784
  return `↳ ❌ ${agentName} — failed\n\n${outcomeText(task.failureReason, "Task failed.")}`;
699
785
  }
700
- if (slackReplySent) {
786
+ if (slackReplySent && !task.tags?.includes("deferred")) {
701
787
  return `↳ ✅ ${agentName} completed`;
702
788
  }
703
789
  return `↳ ✅ ${agentName} — result\n\n${outcomeText(task.output, "Task completed.")}`;
704
790
  }
705
791
 
792
+ // A deferred wake continues a human conversation even though the scheduler
793
+ // assigns source="schedule". Its answer must not depend on delegation cards.
794
+ function isSlackContinuation(task: AgentTask): boolean {
795
+ return (
796
+ task.source === "schedule" &&
797
+ task.taskType === "deferred" &&
798
+ !!task.slackChannelId &&
799
+ !!task.slackThreadTs
800
+ );
801
+ }
802
+
706
803
  /**
707
804
  * True when `task` is a candidate for its own child result card (plan
708
805
  * section 3.4, rules 1-4, 3, and part of rule 6). Callers still need the
@@ -712,6 +809,7 @@ export async function childOutcomeContent(
712
809
  function isChildCardCandidate(task: AgentTask, delegationActivatedAt: string): boolean {
713
810
  return (
714
811
  task.source !== "slack" &&
812
+ !isSlackContinuation(task) &&
715
813
  !!task.slackChannelId &&
716
814
  !!task.slackThreadTs &&
717
815
  (task.status === "completed" || task.status === "failed") &&
@@ -721,9 +819,11 @@ function isChildCardCandidate(task: AgentTask, delegationActivatedAt: string): b
721
819
  );
722
820
  }
723
821
 
724
- function taskNeedsAskOutcome(task: AgentTask, activatedAt: string): boolean {
822
+ function taskNeedsDirectOutcome(task: AgentTask, activatedAt: string): boolean {
725
823
  return (
726
- task.source === "slack" && isAskOutcomeStatus(task.status) && task.createdAt >= activatedAt
824
+ ((task.source === "slack" && isAskOutcomeStatus(task.status)) ||
825
+ (isSlackContinuation(task) && isOutcomeStatus(task.status))) &&
826
+ task.createdAt >= activatedAt
727
827
  );
728
828
  }
729
829
 
@@ -847,7 +947,17 @@ function attachmentLine(attachments: TaskAttachment[]): string | undefined {
847
947
  const attachment = attachments.find((item) => item.isPrimary) ?? attachments[0];
848
948
  if (!attachment) return undefined;
849
949
  const url = taskAttachmentDisplayUrl(attachment);
850
- return /^https?:\/\//.test(url) ? `📎 [${attachment.name}](${url})` : undefined;
950
+ if (!/^https?:\/\//.test(url)) return undefined;
951
+ const label = attachment.name
952
+ .replace(/\s+/g, " ")
953
+ .trim()
954
+ .replace(/[\\[\]()]/g, "\\$&");
955
+ // Keep the Markdown destination intact without changing existing URL escapes.
956
+ const destination = url
957
+ .replace(/[\s\\<>]/g, encodeURIComponent)
958
+ .replace(/\(/g, "%28")
959
+ .replace(/\)/g, "%29");
960
+ return `📎 [${label}](${destination})`;
851
961
  }
852
962
 
853
963
  type MarkdownFence = { character: "`" | "~"; length: number };
@@ -1019,13 +1129,36 @@ export async function streamOutcomeCard(
1019
1129
  reservationWasCreated = reserved.created;
1020
1130
  }
1021
1131
  let streamedFreshContent = false;
1132
+ let deliveredViaFallback = false;
1022
1133
  if (isPendingSlackMessage(outcome)) {
1023
1134
  const reconciled = reservationWasCreated
1024
1135
  ? undefined
1025
1136
  : await findReservedSlackMessage(app.client, outcome, presentation);
1026
1137
  streamedFreshContent = !reconciled;
1027
- const started =
1028
- reconciled ?? (await callSlackWithRetry(app.client, "chat.startStream", startPayload));
1138
+ let started = reconciled;
1139
+ if (!started) {
1140
+ try {
1141
+ started = await callSlackWithRetry(app.client, "chat.startStream", startPayload);
1142
+ } catch (error) {
1143
+ // chat.startStream can fail for reasons callSlackWithRetry does not
1144
+ // retry (e.g. user_not_found). Without a fallback the agent's answer
1145
+ // — already computed above — is dropped entirely. Fall back to a
1146
+ // plain chat.postMessage so a streaming-API error never swallows a
1147
+ // reply.
1148
+ console.error(
1149
+ `[Slack] chat.startStream failed for task ${task.id}; falling back to chat.postMessage:`,
1150
+ error,
1151
+ );
1152
+ started = await callSlackWithRetry(app.client, "chat.postMessage", {
1153
+ channel: task.slackChannelId,
1154
+ thread_ts: task.slackThreadTs,
1155
+ text: presentation,
1156
+ ...(startPayload.username ? { username: startPayload.username } : {}),
1157
+ ...(startPayload.icon_emoji ? { icon_emoji: startPayload.icon_emoji } : {}),
1158
+ });
1159
+ deliveredViaFallback = true;
1160
+ }
1161
+ }
1029
1162
  if (typeof started.ts !== "string" || !started.ts) {
1030
1163
  throw new Error("Slack did not return a timestamp for the outcome stream");
1031
1164
  }
@@ -1035,29 +1168,36 @@ export async function streamOutcomeCard(
1035
1168
  if (!persisted) throw new Error("Failed to persist the outcome stream timestamp");
1036
1169
  outcome = persisted;
1037
1170
  }
1038
- if (!streamedFreshContent) {
1039
- // The stream backing this message was started (or reconciled from) an earlier
1040
- // pass, whose slackReplySent snapshot may have since changed. Overwrite its
1041
- // content with the freshly computed presentation before finalizing, so a
1042
- // completed-then-collapsed reply doesn't finalize with stale full output.
1043
- await callSlackWithRetry(app.client, "chat.update", {
1044
- channel: task.slackChannelId,
1045
- ts: outcome.ts,
1046
- text: presentation,
1047
- });
1048
- }
1049
- try {
1050
- await callSlackWithRetry(app.client, "chat.stopStream", {
1051
- channel: task.slackChannelId,
1052
- ts: outcome.ts,
1053
- blocks: await outcomeFooter(task, tasks, duration),
1054
- });
1055
- } catch (error) {
1056
- // A process may have stopped the stream before it persisted the final
1057
- // permalink. In that one recovery case the message is already immutable,
1058
- // so continue by resolving and recording its permalink.
1059
- if (!isStreamAlreadyStopped(error)) throw error;
1171
+ if (!deliveredViaFallback) {
1172
+ if (!streamedFreshContent) {
1173
+ // The stream backing this message was started (or reconciled from) an earlier
1174
+ // pass, whose slackReplySent snapshot may have since changed. Overwrite its
1175
+ // content with the freshly computed presentation before finalizing, so a
1176
+ // completed-then-collapsed reply doesn't finalize with stale full output.
1177
+ await callSlackWithRetry(app.client, "chat.update", {
1178
+ channel: task.slackChannelId,
1179
+ ts: outcome.ts,
1180
+ text: presentation,
1181
+ });
1182
+ }
1183
+ try {
1184
+ await callSlackWithRetry(app.client, "chat.stopStream", {
1185
+ channel: task.slackChannelId,
1186
+ ts: outcome.ts,
1187
+ blocks: await outcomeFooter(task, tasks, duration),
1188
+ });
1189
+ } catch (error) {
1190
+ // A process may have stopped the stream before it persisted the final
1191
+ // permalink. In that one recovery case the message is already immutable,
1192
+ // so continue by resolving and recording its permalink.
1193
+ if (!isStreamAlreadyStopped(error)) throw error;
1194
+ }
1060
1195
  }
1196
+ // chat.startStream/chat.postMessage posting a new message in the thread is
1197
+ // what normally clears Slack's own "is working…" assistant indicator.
1198
+ // Clear it explicitly too, so a delivery that only ever fails still
1199
+ // doesn't leave the thread stuck spinning.
1200
+ await clearAssistantStatus(app.client, task.slackChannelId, task.slackThreadTs);
1061
1201
  const permalink = await resolvePermalink(app.client, task.slackChannelId, outcome.ts);
1062
1202
  return await updateSlackMessageRecord(outcome.id, {
1063
1203
  permalink,
@@ -1123,13 +1263,14 @@ export async function processSlackRenderV2(): Promise<void> {
1123
1263
  // In-progress asks are eligible only for the timeout backstop. They do
1124
1264
  // not represent immediately active outcome work, so avoid waking an old
1125
1265
  // tree solely to verify its permalink on every render tick.
1126
- const isAskCandidate = taskNeedsAskOutcome(task, activatedAt) && isOutcomeStatus(task.status);
1266
+ const isDirectCandidate =
1267
+ taskNeedsDirectOutcome(task, activatedAt) && isOutcomeStatus(task.status);
1127
1268
  const isChildCandidate =
1128
1269
  delegationEnabled &&
1129
1270
  delegationActivatedAt !== null &&
1130
1271
  isChildCardCandidate(task, delegationActivatedAt) &&
1131
1272
  !task.slackReplySent;
1132
- if (isAskCandidate || isChildCandidate) {
1273
+ if (isDirectCandidate || isChildCandidate) {
1133
1274
  needsOutcome = true;
1134
1275
  break;
1135
1276
  }
@@ -1199,36 +1340,42 @@ export async function processSlackRenderV2(): Promise<void> {
1199
1340
  if (!ownerAsk || ownerAsk.createdAt < delegationActivatedAt) continue;
1200
1341
  if (childCardsThisTick >= CHILD_CARDS_PER_TICK) continue;
1201
1342
  if (askId && (await childCardCountFor(askId)) >= CHILD_CARDS_PER_ASK) continue;
1343
+ if (!outcomeDeliveryGate(task.id)) continue;
1202
1344
  try {
1203
1345
  const outcome = await streamOutcomeCard(task, tree, { buildContent: childOutcomeContent });
1204
1346
  if (outcome) {
1347
+ noteOutcomeDeliverySuccess(task.id);
1205
1348
  childCardsThisTick++;
1206
1349
  if (askId) childCardCounts.set(askId, (childCardCounts.get(askId) ?? 0) + 1);
1207
1350
  await recordSlackDelivery(task, outcome, "child_outcome");
1208
1351
  }
1209
1352
  outcomeCreated ||= !!outcome;
1210
1353
  } catch (error) {
1211
- console.error(`[Slack] Failed to stream child outcome for task ${task.id}:`, error);
1354
+ await noteOutcomeDeliveryFailure(task, error);
1212
1355
  }
1213
1356
  }
1214
1357
 
1215
1358
  for (const task of tasks) {
1216
1359
  if (!isSlackRenderV2Enabled()) return;
1217
- if (task.source !== "slack") continue;
1360
+ if (!taskNeedsDirectOutcome(task, activatedAt)) continue;
1218
1361
  if ((await getSlackOutcomeMessage(task.id))?.finalizedAt) continue;
1219
- if (!isAskOutcomeStatus(task.status) || task.createdAt < activatedAt) continue;
1220
1362
  const deferByClosure =
1363
+ task.source === "slack" &&
1221
1364
  delegationEnabled &&
1222
1365
  delegationActivatedAt !== null &&
1223
1366
  task.createdAt >= delegationActivatedAt;
1224
1367
 
1225
1368
  if (!deferByClosure) {
1369
+ if (!outcomeDeliveryGate(task.id)) continue;
1226
1370
  try {
1227
1371
  const outcome = await streamOutcomeCard(task, tree);
1228
- if (outcome) await finalizeTerminalSlackReactions([task]);
1372
+ if (outcome) {
1373
+ noteOutcomeDeliverySuccess(task.id);
1374
+ await finalizeTerminalSlackReactions([task]);
1375
+ }
1229
1376
  outcomeCreated ||= !!outcome;
1230
1377
  } catch (error) {
1231
- console.error(`[Slack] Failed to stream outcome for task ${task.id}:`, error);
1378
+ await noteOutcomeDeliveryFailure(task, error);
1232
1379
  }
1233
1380
  continue;
1234
1381
  }
@@ -1253,6 +1400,7 @@ export async function processSlackRenderV2(): Promise<void> {
1253
1400
  if (childCardPending) continue;
1254
1401
  const state = closureState(task, closure, new Date(), settleSec, timeoutMin);
1255
1402
  if (state === "open") continue;
1403
+ if (!outcomeDeliveryGate(task.id)) continue;
1256
1404
  try {
1257
1405
  const outcome = await streamOutcomeCard(task, tree, {
1258
1406
  buildContent: (_t, slackReplySent) =>
@@ -1260,6 +1408,7 @@ export async function processSlackRenderV2(): Promise<void> {
1260
1408
  conclusionKind: state === "timedOut" ? "timeout" : "complete",
1261
1409
  });
1262
1410
  if (outcome) {
1411
+ noteOutcomeDeliverySuccess(task.id);
1263
1412
  const app = getSlackApp();
1264
1413
  if (app && task.slackChannelId && task.slackTriggerMessageTs) {
1265
1414
  const reaction = conclusionReactionChoice(state, task, closure);
@@ -1280,7 +1429,7 @@ export async function processSlackRenderV2(): Promise<void> {
1280
1429
  }
1281
1430
  outcomeCreated ||= !!outcome;
1282
1431
  } catch (error) {
1283
- console.error(`[Slack] Failed to stream ask conclusion for task ${task.id}:`, error);
1432
+ await noteOutcomeDeliveryFailure(task, error);
1284
1433
  }
1285
1434
  }
1286
1435
  try {
@@ -1299,4 +1448,5 @@ export function _resetSlackRenderV2ForTests(): void {
1299
1448
  lastTreeUpdateAt.clear();
1300
1449
  treeUpdateTails.clear();
1301
1450
  cachedTeamId = undefined;
1451
+ outcomeDeliveryFailures.clear();
1302
1452
  }
@@ -1,4 +1,5 @@
1
1
  import { getAgentById, getAgentWorkingOnThread, getAllAgents } from "../be/db";
2
+ import type { Agent } from "../types";
2
3
  import { isEnvFlagEnabled } from "../utils/env-flag";
3
4
  import type { AgentMatch } from "./types";
4
5
 
@@ -16,6 +17,15 @@ export function hasOtherUserMention(text: string, botUserId: string): boolean {
16
17
  return mentions.some((m) => m !== `<@${botUserId}>`);
17
18
  }
18
19
 
20
+ /**
21
+ * Build the matches for an explicit swarm#all broadcast.
22
+ */
23
+ export function broadcastMatches(agents: Agent[]): AgentMatch[] {
24
+ return agents
25
+ .filter((agent) => !agent.isLead)
26
+ .map((agent) => ({ agent, matchedText: "swarm#all" }));
27
+ }
28
+
19
29
  /**
20
30
  * Routes a Slack message to the appropriate agent(s) based on mentions.
21
31
  *
@@ -50,10 +60,10 @@ export async function routeMessage(
50
60
 
51
61
  // Check for swarm#all broadcast
52
62
  if (/swarm#all/i.test(text)) {
53
- const nonLeadAgents = agents.filter((a) => !a.isLead);
54
- for (const agent of nonLeadAgents) {
63
+ for (const broadcastMatch of broadcastMatches(agents)) {
64
+ const agent = broadcastMatch.agent;
55
65
  if (!matches.some((m) => m.agent.id === agent.id)) {
56
- matches.push({ agent, matchedText: "swarm#all" });
66
+ matches.push(broadcastMatch);
57
67
  }
58
68
  }
59
69
  }
@@ -20,7 +20,7 @@ async function configuredSteeringTarget(
20
20
  channelId: string,
21
21
  threadTs: string,
22
22
  ): Promise<AgentTask | null> {
23
- switch (process.env.SLACK_THREAD_STEERING) {
23
+ switch (process.env.SLACK_THREAD_STEERING ?? "lead") {
24
24
  case "lead":
25
25
  return getLatestLeadTaskInThread(channelId, threadTs);
26
26
  case "all":
@@ -32,8 +32,8 @@ async function configuredSteeringTarget(
32
32
 
33
33
  /**
34
34
  * Request steering for the configured Slack thread target, if it is currently
35
- * in progress. The default and invalid configuration values deliberately
36
- * return null so Slack preserves its existing task-creation behavior.
35
+ * in progress. Unset configuration targets the lead; explicit off or invalid
36
+ * values return null so Slack preserves its task-creation behavior.
37
37
  */
38
38
  export async function requestSlackThreadSteering(
39
39
  args: SlackThreadSteeringRequest,
@@ -6,10 +6,17 @@ import {
6
6
  } from "../be/db";
7
7
  import { createAdditiveBuffer } from "../tasks/additive-buffer";
8
8
  import { slackContextKey } from "../tasks/context-key";
9
- import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
10
9
  import { getSlackApp } from "./app";
11
10
  import { buildBufferFlushBlocks } from "./blocks";
12
11
  import { rewriteSlackMentions } from "./enrich";
12
+ import type { SlackFile } from "./files";
13
+ import {
14
+ bufferedFileFailures,
15
+ buildEffectiveText,
16
+ createSlackTaskWithFiles,
17
+ fetchSlackFiles,
18
+ notifySlackFileFailures,
19
+ } from "./inbound-files";
13
20
  import { extractSlackMessageText } from "./message-text";
14
21
  import { ensureSlackThreadTree, isSlackRenderV2Enabled } from "./render-v2";
15
22
  import { getAgentDisplayName, getAgentEmoji } from "./responses";
@@ -22,6 +29,7 @@ interface BufferedMessage {
22
29
  ts: string;
23
30
  channelId: string;
24
31
  threadTs: string;
32
+ files?: SlackFile[];
25
33
  }
26
34
 
27
35
  const BUFFER_TIMEOUT_MS = Number(process.env.ADDITIVE_SLACK_BUFFER_MS) || 10_000;
@@ -53,6 +61,7 @@ export function bufferThreadMessage(
53
61
  text: string,
54
62
  userId: string,
55
63
  ts: string,
64
+ files?: SlackFile[],
56
65
  ): void {
57
66
  slackBuffer.enqueue(makeKey(channelId, threadTs), {
58
67
  text,
@@ -60,6 +69,7 @@ export function bufferThreadMessage(
60
69
  ts,
61
70
  channelId,
62
71
  threadTs,
72
+ files,
63
73
  });
64
74
  }
65
75
 
@@ -146,10 +156,33 @@ async function slackFlush(
146
156
 
147
157
  console.log(`[Slack] Flushing buffer: ${key} (${items.length} messages, immediate=${immediate})`);
148
158
 
159
+ // Keep only metadata during the debounce; downloads live for this flush only.
160
+ const app = getSlackApp();
161
+ const files = items.flatMap((item) => item.files ?? []);
162
+ await using inbound = app
163
+ ? await fetchSlackFiles(app.client, files)
164
+ : {
165
+ files,
166
+ fetched: [],
167
+ failed: bufferedFileFailures(files),
168
+ async [Symbol.asyncDispose]() {},
169
+ };
170
+ const resolvedFiles = new Map(inbound.files.map((file) => [file.id, file]));
171
+
149
172
  // Build combined task description. Any in-body `<@U…>` mentions the
150
173
  // requester typed are rewritten via the identity primitive so the agent
151
174
  // sees a name or the explicit UNKNOWN sentinel — never a raw Slack ID.
152
- const combinedText = await rewriteSlackMentions(items.map((m) => m.text).join("\n---\n"));
175
+ const combinedText = await rewriteSlackMentions(
176
+ items
177
+ .map((item) =>
178
+ buildEffectiveText(
179
+ item.text,
180
+ item.files?.map((file) => resolvedFiles.get(file.id) ?? file),
181
+ inbound.failed,
182
+ ),
183
+ )
184
+ .join("\n---\n"),
185
+ );
153
186
  const description = `[Thread follow-up — ${items.length} message(s) buffered]\n\n${combinedText}`;
154
187
 
155
188
  // Find the latest active task in this thread for dependency chaining
@@ -160,18 +193,22 @@ async function slackFlush(
160
193
  );
161
194
  }
162
195
 
163
- const steering = await requestSlackThreadSteering({
164
- channelId,
165
- threadTs,
166
- message: combinedText,
167
- messageTimestamps: items.map((item) => item.ts),
168
- });
196
+ // Steering carries text only. As on the mention path, files need a follow-up
197
+ // task so its attachments exist before a worker can claim it.
198
+ const steering =
199
+ files.length === 0
200
+ ? await requestSlackThreadSteering({
201
+ channelId,
202
+ threadTs,
203
+ message: combinedText,
204
+ messageTimestamps: items.map((item) => item.ts),
205
+ })
206
+ : null;
169
207
  if (steering) {
170
208
  console.log(
171
209
  `[Slack] Buffer flushed → steering ${steering.result.outcome} for task ${steering.task.id}`,
172
210
  );
173
211
 
174
- const app = getSlackApp();
175
212
  const agent = steering.task.agentId ? await getAgentById(steering.task.agentId) : undefined;
176
213
  if (app) {
177
214
  if (!isSlackRenderV2Enabled()) {
@@ -207,19 +244,26 @@ async function slackFlush(
207
244
  const dependsOn = !immediate && latestActiveTask ? [latestActiveTask.id] : undefined;
208
245
 
209
246
  const mostRecentTask = await getMostRecentTaskInThread(channelId, threadTs);
210
- const task = await createTaskWithSiblingAwareness(fullDescription, {
211
- agentId: lead?.id,
212
- routingReason:
213
- lead && mostRecentTask?.agentId === lead.id ? "continuity" : lead ? "skill" : undefined,
214
- source: "slack",
215
- slackChannelId: channelId,
216
- slackThreadTs: threadTs,
217
- slackTriggerMessageTs: items.at(-1)!.ts,
218
- slackUserId: originalRequesterId,
219
- dependsOn,
220
- parentTaskId: mostRecentTask?.id,
221
- contextKey: slackContextKey({ channelId, threadTs }),
222
- });
247
+ const { task, unattached } = await createSlackTaskWithFiles(
248
+ fullDescription,
249
+ {
250
+ agentId: lead?.id,
251
+ routingReason:
252
+ lead && mostRecentTask?.agentId === lead.id ? "continuity" : lead ? "skill" : undefined,
253
+ routingSource: lead ? "engine_default" : undefined,
254
+ source: "slack",
255
+ slackChannelId: channelId,
256
+ slackThreadTs: threadTs,
257
+ slackTriggerMessageTs: items.at(-1)!.ts,
258
+ slackUserId: originalRequesterId,
259
+ dependsOn,
260
+ parentTaskId: mostRecentTask?.id,
261
+ contextKey: slackContextKey({ channelId, threadTs }),
262
+ },
263
+ inbound,
264
+ );
265
+
266
+ if (app) await notifySlackFileFailures(app.client, channelId, threadTs, unattached);
223
267
 
224
268
  console.log(
225
269
  `[Slack] Buffer flushed → task ${task.id} (dependsOn: ${dependsOn ? dependsOn.join(", ") : "none"})`,
@@ -231,7 +275,6 @@ async function slackFlush(
231
275
  }
232
276
 
233
277
  // Slack feedback with Block Kit
234
- const app = getSlackApp();
235
278
  if (app) {
236
279
  const hasDependency = !immediate && !!latestActiveTask;
237
280
  const blocks = buildBufferFlushBlocks({