@desplega.ai/agent-swarm 1.149.0 → 1.150.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 (385) hide show
  1. package/README.md +7 -3
  2. package/dist/{acp-adapter-4jncb126.js → acp-adapter-d5mzggyk.js} +1 -1
  3. package/dist/{actions-31vp3ae7.js → actions-wn6ew0p7.js} +11 -10
  4. package/dist/{anthropic-messages-vs2rabzp.js → anthropic-messages-ec35ktvx.js} +4 -4
  5. package/dist/{app-1hh67x8w.js → app-dty7p9hg.js} +7 -7
  6. package/dist/{artifact-gj9arwrr.js → artifact-td4q4dch.js} +1 -1
  7. package/dist/{assistant-z66dmz6p.js → assistant-q4vhgjtx.js} +16 -17
  8. package/dist/{boot-reembed-wxa8ya0s.js → boot-reembed-92292pgf.js} +9 -9
  9. package/dist/{boot-reembed-fqzxxzjh.js → boot-reembed-n15q6s9x.js} +8 -8
  10. package/dist/{boot-scrub-logs-jnyndh79.js → boot-scrub-logs-44pc955m.js} +5 -5
  11. package/dist/{claude-adapter-3qpa9w0j.js → claude-adapter-cjegjbje.js} +14 -14
  12. package/dist/{claude-managed-adapter-qvsmh6mv.js → claude-managed-adapter-p3ts5m9m.js} +2 -2
  13. package/dist/{claude-sdk-session-2yxgjs5d.js → claude-sdk-session-7ywbn12v.js} +14 -14
  14. package/dist/{cli-cr12paw2.js → cli-111ey3ra.js} +5 -5
  15. package/dist/{cli-4wtq5jjv.js → cli-1mx65de9.js} +1 -1
  16. package/dist/{cli-9d8ntetj.js → cli-2658f6at.js} +1 -1
  17. package/dist/{cli-n3dz2942.js → cli-3ckzxdt1.js} +10 -6
  18. package/dist/{cli-yb9qhqam.js → cli-3mh119qv.js} +184 -67
  19. package/dist/{cli-ykxrnd1q.js → cli-72xdszyx.js} +2 -2
  20. package/dist/{cli-xm87hzbw.js → cli-7fesa1ky.js} +1 -1
  21. package/dist/{cli-3q5ejx9p.js → cli-7kwmt3r5.js} +2 -2
  22. package/dist/{cli-2qts1hys.js → cli-8chp8xab.js} +2 -2
  23. package/dist/{cli-qaacms9y.js → cli-9gqddmxs.js} +122 -97
  24. package/dist/{cli-zvz62chv.js → cli-a45d5v9b.js} +4 -4
  25. package/dist/{cli-9xvdm3bx.js → cli-asvrh7kj.js} +1 -1
  26. package/dist/{cli-kgz4np97.js → cli-b8x19vbh.js} +4 -4
  27. package/dist/{cli-9fy6nk6g.js → cli-ceb5y160.js} +1 -1
  28. package/dist/{cli-k0cr4kat.js → cli-ck1vqzdj.js} +1 -1
  29. package/dist/{cli-e29gmdmw.js → cli-dv6cdjs2.js} +2 -1
  30. package/dist/{cli-jecc01cb.js → cli-dyx0237s.js} +1 -1
  31. package/dist/{cli-kjwt6hdf.js → cli-gm0fhz13.js} +3 -3
  32. package/dist/{cli-sd5pv3b1.js → cli-h505yhex.js} +2 -2
  33. package/dist/{handlers-nbzdzn9k.js → cli-hbagregr.js} +15 -38
  34. package/dist/{cli-08b07b4r.js → cli-hmyynq9b.js} +1 -1
  35. package/dist/{cli-d8ftsp62.js → cli-kt7wevqh.js} +20 -13
  36. package/dist/{cli-gjhjfeeg.js → cli-kyysm7m3.js} +1 -1
  37. package/dist/{cli-1ebpar1q.js → cli-mzb0n642.js} +6 -6
  38. package/dist/{cli-q50ef8g0.js → cli-n1yj5fd5.js} +1 -1
  39. package/dist/{cli-3apbzgyk.js → cli-n75jt793.js} +13 -9
  40. package/dist/{cli-19f354qr.js → cli-q04q68be.js} +2 -2
  41. package/dist/{cli-yab68w40.js → cli-qp69dd5d.js} +29 -14
  42. package/dist/{cli-6pqcb4q0.js → cli-rnsr8asm.js} +12 -11
  43. package/dist/{cli-ygweqcn4.js → cli-rwp3ksm7.js} +91 -108
  44. package/dist/{cli-phsxnkp6.js → cli-ryz8q8p1.js} +74 -58
  45. package/dist/{cli-e95cx1eb.js → cli-taym3dmg.js} +10 -10
  46. package/dist/{cli-nye12xk5.js → cli-ttje5a4z.js} +7 -4
  47. package/dist/{cli-9hspd3dp.js → cli-vr6qj104.js} +1 -1
  48. package/dist/{cli-mnmsfd1w.js → cli-xjb9wghj.js} +7 -3
  49. package/dist/{cli-gv88e1vf.js → cli-ydk0wjj8.js} +1 -1
  50. package/dist/{cli-aae6hz6a.js → cli-zd21jrrc.js} +3 -3
  51. package/dist/{cli-7smrsr25.js → cli-zthatyrx.js} +23 -11
  52. package/dist/cli.js +19 -19
  53. package/dist/{codex-adapter-8meawydj.js → codex-adapter-rn3aq7jm.js} +7 -7
  54. package/dist/{codex-hook-hfrt9shr.js → codex-hook-gthcmm0g.js} +3 -3
  55. package/dist/{codex-session-runner-gjgxyp5j.js → codex-session-runner-bge0cbsw.js} +7 -7
  56. package/dist/{commands-hz3nb95f.js → commands-26phw0pc.js} +5 -5
  57. package/dist/{db-ggz97zbm.js → db-jxjx6s1b.js} +6 -6
  58. package/dist/{e2b-p04vmtx0.js → e2b-413b8t4v.js} +1 -1
  59. package/dist/handlers-vr1djg4n.js +41 -0
  60. package/dist/{hook-bks2q1f6.js → hook-jw0ztn96.js} +9 -9
  61. package/dist/{hook-kq9wp0ep.js → hook-t6fqsggw.js} +10 -10
  62. package/dist/{http-hrf77v87.js → http-hyc7s57s.js} +80 -71
  63. package/dist/{index-n28pw1zt.js → index-1d9r6a74.js} +18 -18
  64. package/dist/{index-cqtebbhb.js → index-bcyx5djw.js} +17 -17
  65. package/dist/{index-6nsbg88z.js → index-sfhaq59d.js} +408 -74
  66. package/dist/{index-8hmeg8j1.js → index-wtwpthe1.js} +17 -17
  67. package/dist/{keepalive-dxc12gvg.js → keepalive-fbsak4ya.js} +7 -7
  68. package/dist/{lead-yckfn18d.js → lead-rdzdm627.js} +39 -39
  69. package/dist/{maintenance-6kfb2m5e.js → maintenance-25z1c1vf.js} +9 -9
  70. package/dist/{node-x1g4v0pq.js → node-209sxmg3.js} +9 -9
  71. package/dist/{oauth-refresh-sweep-1x1ac722.js → oauth-refresh-sweep-j3d7gtse.js} +6 -6
  72. package/dist/{onboard-dcyt1609.js → onboard-dkcwrc75.js} +2 -2
  73. package/dist/{otel-impl-tbk3e5dr.js → otel-impl-8rgyy3j8.js} +1 -1
  74. package/dist/{pi-mono-adapter-zvshk5cj.js → pi-mono-adapter-4s2bbkgq.js} +8 -8
  75. package/dist/{pricing-refresh-kaqg567e.js → pricing-refresh-9cek71nq.js} +7 -7
  76. package/dist/{rbac-roles-cf97v1vd.js → rbac-roles-vk8mqp9v.js} +6 -6
  77. package/dist/{rbac-roles-ammq6rd2.js → rbac-roles-z98s2ypy.js} +5 -5
  78. package/dist/{render-v2-80w603vy.js → render-v2-3j26g0qh.js} +7 -7
  79. package/dist/{seed-pricing-zbrn83xv.js → seed-pricing-7rxhcnzm.js} +6 -6
  80. package/dist/{setup-fe66kczs.js → setup-e2t7vz0c.js} +2 -2
  81. package/dist/work-objects-8d3nd3x2.js +141 -0
  82. package/dist/{worker-qmb2d1e2.js → worker-0x9fn7nq.js} +39 -39
  83. package/openapi.json +15 -4
  84. package/package.json +3 -3
  85. package/src/agentmail/handlers.ts +5 -0
  86. package/src/be/budget-refusal-notify.ts +1 -0
  87. package/src/be/db/tasks/read.ts +3 -0
  88. package/src/be/db/tasks/write.ts +13 -1
  89. package/src/be/db.ts +4 -2
  90. package/src/be/migrations/153_deferred_task_wait_members.sql +25 -0
  91. package/src/be/migrations/154_routing_source.sql +3 -0
  92. package/src/be/scripts/typecheck.ts +7 -3
  93. package/src/be/seed/automation-toggle.ts +69 -0
  94. package/src/be/seed/schedules-seeder.ts +53 -8
  95. package/src/be/seed/workflows-seeder.ts +35 -6
  96. package/src/be/seed-scripts/catalog/delegate.ts +3 -1
  97. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  98. package/src/be/steering.ts +1 -0
  99. package/src/be/swarm-config-guard.ts +1 -0
  100. package/src/commands/credential-refresh.ts +70 -0
  101. package/src/commands/provider-credentials.ts +2 -25
  102. package/src/commands/runner.ts +23 -56
  103. package/src/github/handlers.ts +9 -0
  104. package/src/gitlab/handlers.ts +4 -0
  105. package/src/heartbeat/heartbeat.ts +3 -0
  106. package/src/http/approval-requests.ts +1 -0
  107. package/src/http/apps.ts +1 -0
  108. package/src/http/config.ts +4 -1
  109. package/src/http/core.ts +4 -2
  110. package/src/http/index.ts +16 -3
  111. package/src/http/script-runs.ts +5 -0
  112. package/src/http/tasks.ts +5 -0
  113. package/src/integrations/kapso/inbound.ts +1 -0
  114. package/src/jira/sync.ts +2 -0
  115. package/src/linear/sync.ts +2 -0
  116. package/src/prompts/session-templates.ts +13 -9
  117. package/src/providers/claude-adapter.ts +21 -8
  118. package/src/scheduler/deferred-task-waits.ts +34 -5
  119. package/src/scheduler/schedule-task.ts +5 -1
  120. package/src/scheduler/scheduler.ts +104 -123
  121. package/src/scripts-runtime/types/stdlib.d.ts +7 -3
  122. package/src/scripts-runtime/types/swarm-sdk.d.ts +7 -3
  123. package/src/slack/HEURISTICS.md +2 -0
  124. package/src/slack/actions.ts +1 -0
  125. package/src/slack/app.ts +2 -0
  126. package/src/slack/assistant.ts +4 -12
  127. package/src/slack/handlers.ts +9 -14
  128. package/src/slack/inbound-files.ts +3 -3
  129. package/src/slack/render-v2.ts +17 -3
  130. package/src/slack/thread-buffer.ts +66 -23
  131. package/src/slack/work-objects.test.ts +252 -0
  132. package/src/slack/work-objects.ts +118 -0
  133. package/src/tasks/worker-follow-up.ts +4 -0
  134. package/src/tests/acp-adapter.test.ts +10 -10
  135. package/src/tests/agent-fs-live-url.test.ts +40 -0
  136. package/src/tests/agent-fs-provision-seeder.test.ts +12 -12
  137. package/src/tests/api-key-tracking.test.ts +18 -18
  138. package/src/tests/api-key.test.ts +4 -2
  139. package/src/tests/apps-rbac.test.ts +1 -1
  140. package/src/tests/apps-spike5.test.ts +1 -1
  141. package/src/tests/apps-sync-engine.test.ts +8 -8
  142. package/src/tests/apps-user-config.test.ts +1 -1
  143. package/src/tests/artifact-sdk.test.ts +8 -8
  144. package/src/tests/asset-key-mcp.test.ts +2 -0
  145. package/src/tests/automation-preflight-alert.test.ts +1 -1
  146. package/src/tests/base-prompt.test.ts +6 -6
  147. package/src/tests/budget-user-scope.test.ts +1 -1
  148. package/src/tests/budgets-routes.test.ts +1 -1
  149. package/src/tests/claude-adapter-binary.test.ts +22 -40
  150. package/src/tests/claude-adapter-otel.test.ts +13 -7
  151. package/src/tests/claude-adapter.test.ts +8 -6
  152. package/src/tests/claude-hook-credentials.test.ts +3 -3
  153. package/src/tests/claude-managed-adapter.test.ts +4 -4
  154. package/src/tests/claude-managed-setup.test.ts +3 -3
  155. package/src/tests/claude-sdk-lifecycle.test.ts +1 -1
  156. package/src/tests/claude-sdk-transport.test.ts +6 -6
  157. package/src/tests/claude-stop-hook.test.ts +8 -8
  158. package/src/tests/claude-worker-parity.test.ts +3 -3
  159. package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +5 -5
  160. package/src/tests/codex-adapter.test.ts +7 -5
  161. package/src/tests/codex-app-server.test.ts +2 -2
  162. package/src/tests/codex-login.test.ts +15 -15
  163. package/src/tests/codex-oauth-credential-info.test.ts +4 -4
  164. package/src/tests/codex-oauth-keep-warm.test.ts +4 -4
  165. package/src/tests/codex-oauth-refresh-lock.test.ts +1 -1
  166. package/src/tests/codex-oauth-storage.test.ts +7 -7
  167. package/src/tests/codex-pool.test.ts +1 -1
  168. package/src/tests/codex-steering-hook.test.ts +1 -1
  169. package/src/tests/codex-swarm-events.test.ts +1 -1
  170. package/src/tests/config-api-only-keys.test.ts +1 -1
  171. package/src/tests/config-session-auth.test.ts +2 -2
  172. package/src/tests/connection-embedded-auth.test.ts +20 -18
  173. package/src/tests/core-auth.test.ts +1 -1
  174. package/src/tests/create-page-tool.test.ts +3 -3
  175. package/src/tests/credential-refresh.test.ts +115 -0
  176. package/src/tests/credential-wait.test.ts +2 -2
  177. package/src/tests/credentials.test.ts +6 -4
  178. package/src/tests/db-queries-oauth.test.ts +40 -40
  179. package/src/tests/defer-task.test.ts +73 -8
  180. package/src/tests/deferred-task-wait-migration.test.ts +64 -0
  181. package/src/tests/deferred-task-wake.test.ts +190 -1
  182. package/src/tests/devin-adapter.test.ts +1 -1
  183. package/src/tests/devin-api.test.ts +1 -1
  184. package/src/tests/e2b-dispatch.test.ts +21 -19
  185. package/src/tests/e2e-claude-transport.test.ts +3 -3
  186. package/src/tests/e2e-redact.test.ts +6 -6
  187. package/src/tests/ensure-token.test.ts +68 -68
  188. package/src/tests/entrypoint-api-readiness.test.ts +1 -1
  189. package/src/tests/entrypoint-codex-oauth-seed.test.ts +13 -13
  190. package/src/tests/events-http.test.ts +1 -1
  191. package/src/tests/fs-routes.test.ts +1 -1
  192. package/src/tests/gitlab-auth.test.ts +10 -10
  193. package/src/tests/harness-provider-resolution.test.ts +8 -5
  194. package/src/tests/heartbeat-reroute-decision.test.ts +1 -0
  195. package/src/tests/helper-credentials.test.ts +137 -0
  196. package/src/tests/http/context-routes.test.ts +2 -2
  197. package/src/tests/http-api-integration.test.ts +5 -2
  198. package/src/tests/http-users.test.ts +1 -1
  199. package/src/tests/identity-refresh.test.ts +2 -2
  200. package/src/tests/internal-ai/complete-structured.test.ts +2 -2
  201. package/src/tests/internal-ai/credentials.test.ts +19 -17
  202. package/src/tests/jira-metadata.test.ts +1 -1
  203. package/src/tests/jira-oauth.test.ts +5 -5
  204. package/src/tests/jira-sync.test.ts +1 -1
  205. package/src/tests/jira-webhook-lifecycle.test.ts +3 -3
  206. package/src/tests/jira-webhook.test.ts +2 -2
  207. package/src/tests/kapso-inbound.test.ts +2 -2
  208. package/src/tests/kv-http.test.ts +1 -1
  209. package/src/tests/kv-namespace-resolution.test.ts +1 -1
  210. package/src/tests/kv-page-proxy.test.ts +2 -2
  211. package/src/tests/launch-password-rejection.test.ts +2 -2
  212. package/src/tests/linear-webhook.test.ts +1 -1
  213. package/src/tests/mcp-oauth-dcr-reuse.test.ts +6 -6
  214. package/src/tests/mcp-oauth-ensure-token.test.ts +28 -22
  215. package/src/tests/mcp-oauth-manual-client.test.ts +16 -12
  216. package/src/tests/mcp-oauth-queries.test.ts +35 -32
  217. package/src/tests/mcp-oauth-resolve-secrets.test.ts +8 -8
  218. package/src/tests/mcp-oauth-wrapper.test.ts +20 -20
  219. package/src/tests/mcp-user-route.test.ts +1 -1
  220. package/src/tests/memory-edges.test.ts +1 -1
  221. package/src/tests/memory-rate-endpoint.test.ts +1 -1
  222. package/src/tests/memory-rate-tool.test.ts +2 -2
  223. package/src/tests/memory-rater-e2e.test.ts +1 -1
  224. package/src/tests/memory-rater-llm-summarizer.test.ts +3 -3
  225. package/src/tests/memory-rater-llm.test.ts +1 -1
  226. package/src/tests/model-control.test.ts +3 -0
  227. package/src/tests/multi-runtime-registration.test.ts +97 -3
  228. package/src/tests/oauth-access-token-tool.test.ts +18 -18
  229. package/src/tests/oauth-callback-flow.test.ts +8 -8
  230. package/src/tests/oauth-credential-bindings.test.ts +29 -27
  231. package/src/tests/oauth-keepalive.test.ts +10 -10
  232. package/src/tests/oauth-presets.test.ts +13 -5
  233. package/src/tests/oauth-refresh-failure.test.ts +9 -9
  234. package/src/tests/oauth-refresh-sweep.test.ts +20 -13
  235. package/src/tests/oauth-wrapper.test.ts +1 -1
  236. package/src/tests/onboard-compose.test.ts +3 -3
  237. package/src/tests/onboard-env.test.ts +26 -26
  238. package/src/tests/onboard-non-interactive.test.ts +4 -4
  239. package/src/tests/opencode-adapter.test.ts +1 -1
  240. package/src/tests/opencode-plugin.test.ts +2 -2
  241. package/src/tests/otel-session-cost-metrics.test.ts +1 -1
  242. package/src/tests/page-proxy-authed.test.ts +2 -2
  243. package/src/tests/page-proxy.test.ts +2 -2
  244. package/src/tests/page-session.test.ts +7 -7
  245. package/src/tests/page-viewer-auth.test.ts +2 -2
  246. package/src/tests/pages-authed-mode.test.ts +1 -1
  247. package/src/tests/pages-http.test.ts +1 -1
  248. package/src/tests/pages-password-mode.test.ts +1 -1
  249. package/src/tests/pages-public-authed-401.test.ts +1 -1
  250. package/src/tests/pages-public-html.test.ts +2 -2
  251. package/src/tests/pages-view-count.test.ts +1 -1
  252. package/src/tests/pi-mono-adapter.test.ts +10 -10
  253. package/src/tests/pi-mono-extension.test.ts +2 -2
  254. package/src/tests/pricing-routes.test.ts +1 -1
  255. package/src/tests/profile-sync-warning.test.ts +8 -8
  256. package/src/tests/profile-sync.test.ts +8 -8
  257. package/src/tests/prompt-template-session.test.ts +36 -5
  258. package/src/tests/provider-process-group.test.ts +95 -4
  259. package/src/tests/providers/pi-cost.test.ts +1 -1
  260. package/src/tests/queue-stall-alarm.test.ts +1 -1
  261. package/src/tests/rbac-admission.test.ts +1 -1
  262. package/src/tests/rbac-charact-http.test.ts +1 -1
  263. package/src/tests/rbac-e2e-helpers.ts +1 -1
  264. package/src/tests/realtime-room-surfaces.test.ts +1 -1
  265. package/src/tests/reload-config.test.ts +1 -1
  266. package/src/tests/routing-reason-contract.test.ts +76 -3
  267. package/src/tests/routing-reason-inventory.test.ts +11 -6
  268. package/src/tests/routing-source.test.ts +53 -0
  269. package/src/tests/runner-agent-fs-provision-retry.test.ts +1 -1
  270. package/src/tests/runner-context-preamble.test.ts +1 -1
  271. package/src/tests/runner-deployment-resume.test.ts +1 -1
  272. package/src/tests/runner-fallback-output.test.ts +1 -1
  273. package/src/tests/runner-identity-refresh.test.ts +1 -1
  274. package/src/tests/runner-skills-refresh.test.ts +1 -1
  275. package/src/tests/schedule-target-type.test.ts +3 -3
  276. package/src/tests/scheduled-tasks.test.ts +14 -5
  277. package/src/tests/scheduler-occurrence.test.ts +347 -0
  278. package/src/tests/script-apis-mcp.test.ts +1 -1
  279. package/src/tests/script-connections-http.test.ts +61 -58
  280. package/src/tests/script-connections-mcp.test.ts +1 -1
  281. package/src/tests/script-connections.test.ts +1 -1
  282. package/src/tests/script-runs-http.test.ts +15 -7
  283. package/src/tests/script-workflows-runtime-e2e.test.ts +1 -1
  284. package/src/tests/scripts-external-api.test.ts +1 -1
  285. package/src/tests/scripts-http.test.ts +1 -1
  286. package/src/tests/scripts-mcp-e2e.test.ts +1 -1
  287. package/src/tests/scripts-only-gating.test.ts +2 -2
  288. package/src/tests/scripts-retention.test.ts +1 -1
  289. package/src/tests/scripts-runtime-identity.test.ts +1 -1
  290. package/src/tests/scripts-runtime-secret-egress.test.ts +1 -1
  291. package/src/tests/scripts-runtime.test.ts +6 -4
  292. package/src/tests/scripts-typecheck.test.ts +21 -0
  293. package/src/tests/sdk-allowlist.test.ts +1 -1
  294. package/src/tests/secret-scrubber.test.ts +38 -31
  295. package/src/tests/seed-registry.test.ts +46 -8
  296. package/src/tests/seed-schedules.test.ts +139 -8
  297. package/src/tests/seed-workflows.test.ts +93 -7
  298. package/src/tests/send-task-output-schema.test.ts +9 -1
  299. package/src/tests/send-task-requested-by.test.ts +1 -0
  300. package/src/tests/send-task-slack-routing-guard.test.ts +2 -0
  301. package/src/tests/session-costs-codex-recompute.test.ts +1 -1
  302. package/src/tests/session-costs-golden.test.ts +2 -2
  303. package/src/tests/session-costs-model-key-normalize.test.ts +2 -2
  304. package/src/tests/session-costs-recompute-all-providers.test.ts +1 -1
  305. package/src/tests/session-tokens.test.ts +1 -1
  306. package/src/tests/shutdown-error-scrubbing.test.ts +1 -1
  307. package/src/tests/slack-file-tools-attachments.test.ts +1 -1
  308. package/src/tests/slack-inbound-files.test.ts +222 -86
  309. package/src/tests/slack-reaction-config.test.ts +5 -4
  310. package/src/tests/slack-read-boundaries.test.ts +1 -1
  311. package/src/tests/slack-render-v2.test.ts +120 -1
  312. package/src/tests/status.test.ts +7 -7
  313. package/src/tests/steering-core.test.ts +1 -1
  314. package/src/tests/steering-transport.test.ts +4 -4
  315. package/src/tests/store-progress-output-guidance.test.ts +34 -0
  316. package/src/tests/swarm-tool-result-gate.test.ts +11 -11
  317. package/src/tests/swarm-x-tool.test.ts +7 -5
  318. package/src/tests/sync-ai-toolbox-skills.test.ts +19 -0
  319. package/src/tests/task-supersede-resume.test.ts +1 -1
  320. package/src/tests/task-tools-ctx.test.ts +2 -0
  321. package/src/tests/tasks-requested-by-filter.test.ts +2 -2
  322. package/src/tests/telemetry-init.test.ts +11 -3
  323. package/src/tests/tool-output-agent-id.test.ts +1 -0
  324. package/src/tests/tracker-fold.test.ts +7 -7
  325. package/src/tests/tracker-tools.test.ts +3 -3
  326. package/src/tests/ui-followup-lead-delegation.test.ts +1 -0
  327. package/src/tests/unified-oauth-migration.test.ts +5 -5
  328. package/src/tests/user-token-rest-auth.test.ts +1 -1
  329. package/src/tests/user-token-routes.test.ts +1 -1
  330. package/src/tests/whoami-route.test.ts +1 -1
  331. package/src/tests/workflow-agent-task.test.ts +15 -13
  332. package/src/tests/workflow-ctx.test.ts +1 -1
  333. package/src/tests/workflow-e2e.test.ts +1 -1
  334. package/src/tests/workflow-engine-v2.test.ts +3 -1
  335. package/src/tests/workflow-executors.test.ts +2 -2
  336. package/src/tests/workflow-http-v2.test.ts +8 -6
  337. package/src/tests/workflow-input-redaction.test.ts +7 -5
  338. package/src/tests/workflow-integration-io.test.ts +2 -2
  339. package/src/tests/workflow-llm.test.ts +5 -5
  340. package/src/tests/workflow-registry.test.ts +1 -1
  341. package/src/tests/workflow-swarm-script.test.ts +1 -1
  342. package/src/tests/workflow-triggers-v2.test.ts +23 -21
  343. package/src/tests/x-composio.test.ts +9 -7
  344. package/src/tests/x402-client.test.ts +1 -1
  345. package/src/tests/x402-config.test.ts +28 -41
  346. package/src/tools/defer-task.ts +156 -24
  347. package/src/tools/send-task.ts +21 -1
  348. package/src/tools/skills/skill-publish.ts +1 -0
  349. package/src/tools/store-progress.ts +1 -1
  350. package/src/tools/templates.ts +2 -1
  351. package/src/types.ts +11 -0
  352. package/src/utils/constants.ts +7 -1
  353. package/src/utils/credentials.test.ts +16 -16
  354. package/src/workflows/executors/agent-task.ts +5 -0
  355. package/templates/ai-toolbox.manifest.json +7 -1
  356. package/templates/schedules/daily-blocker-digest/config.json +6 -1
  357. package/templates/schedules/daily-compounding-reflection/config.json +1 -0
  358. package/templates/schedules/daily-hn-briefing/config.json +1 -1
  359. package/templates/schedules/daily-status-report/config.json +6 -1
  360. package/templates/schedules/daily-swarm-update-check/config.json +19 -0
  361. package/templates/schedules/daily-swarm-update-check/content.md +49 -0
  362. package/templates/schedules/daily-workflow-health-audit/config.json +6 -1
  363. package/templates/schedules/gtm-weekly-review/config.json +1 -1
  364. package/templates/schedules/weekly-code-health-reports/config.json +1 -1
  365. package/templates/schedules/weekly-dependabot-triage/config.json +1 -1
  366. package/templates/schedules/weekly-dora-metrics/config.json +1 -1
  367. package/templates/schedules/weekly-harness-upgrade-check/config.json +1 -1
  368. package/templates/skills/delegate-work/files/scripts/codex-exec.sh +1 -1
  369. package/templates/skills/serply-search/SKILL.md +95 -0
  370. package/templates/skills/serply-search/config.json +13 -0
  371. package/templates/skills/serply-search/content.md +90 -0
  372. package/templates/skills/swarm-scripts/SKILL.md +2 -2
  373. package/templates/skills/swarm-scripts/content.md +2 -2
  374. package/templates/workflows/claude-code-changelog-watch/config.json +1 -1
  375. package/templates/workflows/gsc-topic-miner/config.json +1 -1
  376. package/templates/workflows/linear-drain-loop/config.json +1 -1
  377. package/templates/workflows/llm-safe-release-context/config.json +1 -1
  378. package/templates/workflows/ralph-loop/config.json +1 -1
  379. package/dist/{azure-openai-responses-cqar64rc.js → azure-openai-responses-g85jv7m8.js} +8 -8
  380. package/dist/{cli-4xz3pmrq.js → cli-8qx7jnrk.js} +5 -5
  381. package/dist/{cli-6664w7y4.js → cli-wk0gjrn3.js} +3 -3
  382. package/dist/{google-vertex-ppw8ydpa.js → google-vertex-pkhpkbq9.js} +3 -3
  383. package/dist/{openai-codex-responses-g7w48y30.js → openai-codex-responses-pm5gnyfa.js} +4 -4
  384. package/dist/{openai-completions-0bqmsajw.js → openai-completions-qdcmkfnr.js} +12 -12
  385. package/dist/{openai-responses-nmeyptke.js → openai-responses-vwvk1taj.js} +13 -13
@@ -19,8 +19,8 @@ import { handleSessionData } from "../http/session-data";
19
19
  import { getPathSegments, parseQueryParams } from "../http/utils";
20
20
  import { listenOnFreePort } from "./test-net";
21
21
 
22
- const TEST_DB_PATH = "./test-model-key-normalize.sqlite";
23
- const API_KEY = "test-model-key-normalize";
22
+ const TEST_DB_PATH = "./example-test-model-key-normalize.sqlite";
23
+ const API_KEY = "example-test-model-key-normalize";
24
24
 
25
25
  async function removeDbFiles(path: string): Promise<void> {
26
26
  for (const suffix of ["", "-wal", "-shm"]) {
@@ -17,7 +17,7 @@ import { getPathSegments, parseQueryParams } from "../http/utils";
17
17
  import { listenOnFreePort } from "./test-net";
18
18
 
19
19
  const TEST_DB_PATH = "./test-recompute-all-providers.sqlite";
20
- const API_KEY = "test-recompute-all";
20
+ const API_KEY = "example-test-recompute-all";
21
21
 
22
22
  async function removeDbFiles(path: string): Promise<void> {
23
23
  for (const suffix of ["", "-wal", "-shm"]) {
@@ -23,7 +23,7 @@ import { handleSessions } from "../http/sessions";
23
23
  import { getPathSegments, parseQueryParams } from "../http/utils";
24
24
  import { listenOnFreePort } from "./test-net";
25
25
 
26
- const API_KEY = "test-session-token-key";
26
+ const API_KEY = "example-test-session-token-key";
27
27
 
28
28
  function createTestServer(apiKey: string): Server {
29
29
  return createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
@@ -18,7 +18,7 @@ import {
18
18
  * rather than executed.
19
19
  */
20
20
 
21
- const SECRET = "xoxb-9911-shutdown-token-value";
21
+ const SECRET = "example-shutdown-token-value";
22
22
 
23
23
  const shutdownSourcePath = `${import.meta.dir}/../http/index.ts`;
24
24
  const shutdownSource = await Bun.file(shutdownSourcePath).text();
@@ -21,7 +21,7 @@ import type { SlackFile } from "../slack/files";
21
21
  import { createSlackTaskWithFiles, fetchSlackFiles } from "../slack/inbound-files";
22
22
 
23
23
  const TEST_DB_PATH = "./test-slack-file-tools-attachments.sqlite";
24
- const BOT_TOKEN = "xoxb-file-tools-test";
24
+ const BOT_TOKEN = "example-slack-file-tools-token";
25
25
  const AGENT_ID = "aaaaaaaa-0000-4000-8000-00000000f001";
26
26
  const OTHER_ID = "aaaaaaaa-0000-4000-8000-00000000f002";
27
27
  const PNG = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 9, 9]);
@@ -6,7 +6,17 @@
6
6
  * task unclaimable until its attachments exist, and both Slack ingress
7
7
  * handlers end to end against a real DB and the local file provider.
8
8
  */
9
- import { afterAll, beforeAll, beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
9
+ import {
10
+ afterAll,
11
+ afterEach,
12
+ beforeAll,
13
+ beforeEach,
14
+ describe,
15
+ expect,
16
+ mock,
17
+ spyOn,
18
+ test,
19
+ } from "bun:test";
10
20
  import { mkdtemp, rm, unlink } from "node:fs/promises";
11
21
  import { tmpdir } from "node:os";
12
22
  import { join } from "node:path";
@@ -16,13 +26,17 @@ import {
16
26
  createTaskExtended,
17
27
  getDbClient,
18
28
  getLogsByTaskId,
29
+ getSlackTasksInThread,
30
+ getSteeringMessagesForTask,
19
31
  getTaskAttachments,
20
32
  getTaskById,
21
33
  initDb,
22
34
  promoteAbandonedDraftTasks,
35
+ startTask,
23
36
  } from "../be/db";
24
37
  import { MAX_TASK_ATTACHMENT_BYTES } from "../be/task-attachment-store";
25
38
  import { getFileStorageProvider, resetFileStorageProviderForTests } from "../fs/registry";
39
+ import * as slackAppModule from "../slack/app";
26
40
  import { createAssistant } from "../slack/assistant";
27
41
  import * as slackEnrichModule from "../slack/enrich";
28
42
  import type { SlackFile } from "../slack/files";
@@ -34,10 +48,10 @@ import {
34
48
  notifySlackFileFailures,
35
49
  setDraftLeaseRefreshMsForTests,
36
50
  } from "../slack/inbound-files";
37
- import { instantFlush } from "../slack/thread-buffer";
51
+ import { getBufferMessageCount, instantFlush } from "../slack/thread-buffer";
38
52
 
39
53
  const TEST_DB_PATH = "./test-slack-inbound-files.sqlite";
40
- const BOT_TOKEN = "xoxb-inbound-files-test";
54
+ const BOT_TOKEN = "example-slack-inbound-files-token";
41
55
  const PNG_BYTES = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1, 2, 3]);
42
56
 
43
57
  let slackFiles: ReturnType<typeof Bun.serve>;
@@ -51,6 +65,7 @@ const previousEnv = {
51
65
  SLACK_RENDER_V2: process.env.SLACK_RENDER_V2,
52
66
  STEERING_ENABLED: process.env.STEERING_ENABLED,
53
67
  SLACK_THREAD_STEERING: process.env.SLACK_THREAD_STEERING,
68
+ SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION: process.env.SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION,
54
69
  };
55
70
 
56
71
  const HTML_FILE = new TextEncoder().encode("<html><body>quarterly report</body></html>");
@@ -708,113 +723,234 @@ describe("Slack ingress with files", () => {
708
723
  expect(await getTaskAttachments(followUp!.id)).toHaveLength(1);
709
724
  });
710
725
 
711
- test("a follow-up queued by ADDITIVE_SLACK says its file was not attached", async () => {
712
- await createAgent({ name: "lead", isLead: true, status: "busy" });
713
- const { task: first } = await sendAssistantDm({ subtype: undefined, text: "start" });
714
- await getDbClient().run("UPDATE agent_tasks SET status = 'in_progress' WHERE id = ?", [
715
- first!.id,
716
- ]);
717
- process.env.ADDITIVE_SLACK = "true";
718
- // instantFlush steers into the running session when steering is on; this
719
- // asserts the task-creating path, so keep steering off whatever the env says.
720
- process.env.STEERING_ENABLED = "false";
721
- delete process.env.SLACK_THREAD_STEERING;
722
- const client = slackClient();
726
+ describe("buffered follow-ups with files", () => {
727
+ let earlier: Awaited<ReturnType<typeof createTaskExtended>>;
728
+ let client: ReturnType<typeof slackClient>;
729
+ const getApp = spyOn(slackAppModule, "getSlackApp");
730
+ const channelId = "C0BUFFERFILES";
731
+ const threadTs = "1950000000.000001";
732
+ const key = `${channelId}:${threadTs}`;
733
+
734
+ beforeEach(async () => {
735
+ const lead = await createAgent({
736
+ name: "lead",
737
+ isLead: true,
738
+ status: "busy",
739
+ harnessProvider: "pi",
740
+ });
741
+ earlier = await createTaskExtended("earlier work", {
742
+ agentId: lead.id,
743
+ source: "slack",
744
+ slackChannelId: channelId,
745
+ slackThreadTs: threadTs,
746
+ });
747
+ await startTask(earlier.id);
748
+ process.env.ADDITIVE_SLACK = "true";
749
+ process.env.STEERING_ENABLED = "true";
750
+ process.env.SLACK_THREAD_STEERING = "lead";
751
+ process.env.SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION = "false";
752
+ client = slackClient();
753
+ getApp.mockReturnValue({ client } as never);
754
+ });
723
755
 
724
- try {
756
+ afterEach(async () => {
757
+ await instantFlush(key);
758
+ process.env.ADDITIVE_SLACK = "false";
759
+ restoreSteeringEnv();
760
+ const requireMention = previousEnv.SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION;
761
+ if (requireMention === undefined) delete process.env.SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION;
762
+ else process.env.SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION = requireMention;
763
+ getApp.mockReturnValue(null);
764
+ });
765
+
766
+ afterAll(() => getApp.mockRestore());
767
+
768
+ async function sendBuffered(text: string, files: SlackFile[] = []) {
725
769
  seq += 1;
726
- await assistantMessage({
727
- message: {
728
- channel: first!.slackChannelId,
729
- thread_ts: first!.slackThreadTs,
730
- ts: `1800000000.${String(seq).padStart(6, "0")}`,
770
+ const ts = `1950000001.${String(seq).padStart(6, "0")}`;
771
+ await channelMessage({
772
+ event: {
773
+ type: "message",
774
+ subtype: files.length ? "file_share" : undefined,
775
+ channel: channelId,
776
+ thread_ts: threadTs,
777
+ ts,
731
778
  user: "U_HUMAN",
732
- subtype: "file_share",
733
- text: "",
734
- files: [slackFile()],
779
+ text,
780
+ files,
735
781
  },
736
782
  body: { event_id: `evt_inbound_files_${seq}` },
737
783
  client,
738
784
  say: mock(async () => ({})),
739
- setStatus: mock(async () => {}),
740
- setTitle: mock(async () => {}),
741
- getThreadContext: mock(async () => ({})),
742
785
  });
743
- } finally {
744
- process.env.ADDITIVE_SLACK = "false";
786
+ return ts;
745
787
  }
746
788
 
747
- try {
748
- await assertQueuedFollowUp();
749
- } finally {
750
- restoreSteeringEnv();
751
- }
752
-
753
- async function assertQueuedFollowUp() {
754
- expect(fileRequests).toEqual([]);
755
- expect(client.chat.postMessage).toHaveBeenCalledTimes(1);
756
- const [args] = client.chat.postMessage.mock.calls[0] as unknown as [{ text: string }];
757
- expect(args.text).toContain("`screenshot.png` (follow-ups queued by ADDITIVE_SLACK");
758
-
759
- // Drain the debounce buffer now, while the DB is still open.
760
- await instantFlush(`${first!.slackChannelId}:${first!.slackThreadTs}`);
761
- const queued = await getDbClient().get<{ task: string }>(
762
- "SELECT task FROM agent_tasks WHERE slackThreadTs = ? AND id != ? ORDER BY createdAt DESC",
763
- [first!.slackThreadTs, first!.id],
789
+ async function followUp() {
790
+ const tasks = (await getSlackTasksInThread(channelId, threadTs)).filter(
791
+ (task) => task.id !== earlier.id,
764
792
  );
765
- expect(queued?.task).toContain("(not attached: follow-ups queued by ADDITIVE_SLACK");
793
+ expect(tasks).toHaveLength(1);
794
+ return tasks[0]!;
766
795
  }
767
- });
768
796
 
769
- test("a !now follow-up with a file says the file was not attached", async () => {
770
- const lead = await createAgent({ name: "lead", isLead: true, status: "busy" });
771
- const threadTs = "1950000000.000001";
772
- const earlier = await createTaskExtended("earlier work", {
773
- agentId: lead.id,
774
- source: "slack",
775
- slackChannelId: "C0NOWTEST1",
776
- slackThreadTs: threadTs,
777
- });
778
- await getDbClient().run("UPDATE agent_tasks SET status = 'in_progress' WHERE id = ?", [
779
- earlier.id,
780
- ]);
781
- process.env.ADDITIVE_SLACK = "true";
782
- process.env.STEERING_ENABLED = "false";
783
- delete process.env.SLACK_THREAD_STEERING;
784
- const client = slackClient();
785
- seq += 1;
797
+ function failureNotices() {
798
+ return (client.chat.postMessage.mock.calls as unknown as [{ text: string }][])
799
+ .map(([args]) => args.text)
800
+ .filter((text) => text.includes("Couldn't attach"));
801
+ }
786
802
 
787
- try {
788
- await channelMessage({
789
- event: {
790
- type: "message",
791
- subtype: "file_share",
792
- channel: "C0NOWTEST1",
803
+ test("assistant follow-ups retain files until flush and resolve id-only metadata", async () => {
804
+ client.files.info.mockResolvedValue({ ok: true, file: slackFile() } as never);
805
+ seq += 1;
806
+ await assistantMessage({
807
+ message: {
808
+ channel: channelId,
793
809
  thread_ts: threadTs,
794
810
  ts: `1950000001.${String(seq).padStart(6, "0")}`,
795
811
  user: "U_HUMAN",
796
- text: "!now here it is",
797
- files: [slackFile()],
812
+ subtype: "file_share",
813
+ text: "",
814
+ files: [{ id: "F0SHOT0001" }],
798
815
  },
799
816
  body: { event_id: `evt_inbound_files_${seq}` },
800
817
  client,
801
818
  say: mock(async () => ({})),
819
+ setStatus: mock(async () => {}),
820
+ setTitle: mock(async () => {}),
821
+ getThreadContext: mock(async () => ({})),
822
+ });
823
+ expect(fileRequests).toEqual([]);
824
+ expect(client.files.info).not.toHaveBeenCalled();
825
+ expect(failureNotices()).toEqual([]);
826
+
827
+ await instantFlush(key);
828
+ const task = await followUp();
829
+ expect(await readStoredBytes(task.id)).toEqual([PNG_BYTES]);
830
+ expect(task.task).toContain("[File: screenshot.png");
831
+ expect(task.task).not.toContain("not attached");
832
+ expect(await wasDraft(task.id)).toBe(true);
833
+ expect(await getSteeringMessagesForTask(earlier.id)).toEqual([]);
834
+ expect(failureNotices()).toEqual([]);
835
+ });
836
+
837
+ for (const steering of ["off", "lead"]) {
838
+ test(`timed flush attaches every message's files with steering ${steering}`, async () => {
839
+ process.env.SLACK_THREAD_STEERING = steering;
840
+ const nativeSetTimeout = globalThis.setTimeout;
841
+ const bufferMs = Number(process.env.ADDITIVE_SLACK_BUFFER_MS) || 10_000;
842
+ const timer = spyOn(globalThis, "setTimeout").mockImplementation((fn, ms, ...args) =>
843
+ nativeSetTimeout(fn, ms === bufferMs ? 250 : ms, ...args),
844
+ );
845
+ const otherBytes = new Uint8Array([...PNG_BYTES, 4]);
846
+ bytesById.set("F0OTHER0001", otherBytes);
847
+ let lastTs: string;
848
+ try {
849
+ await sendBuffered("", [slackFile()]);
850
+ lastTs = await sendBuffered("compare these", [
851
+ slackFile({ id: "F0OTHER0001", name: "other.png", size: otherBytes.length }),
852
+ ]);
853
+ expect(getBufferMessageCount(key)).toBe(2);
854
+ expect(fileRequests).toEqual([]);
855
+ expect(failureNotices()).toEqual([]);
856
+ } finally {
857
+ timer.mockRestore();
858
+ }
859
+ // Wait for the real timer callback and draft promotion, not instantFlush.
860
+ for (let i = 0; i < 200; i++) {
861
+ const tasks = await getSlackTasksInThread(channelId, threadTs);
862
+ if (tasks.some((task) => task.id !== earlier.id && task.status === "pending")) break;
863
+ await Bun.sleep(10);
864
+ }
865
+ const task = await followUp();
866
+ expect(task.status).toBe("pending");
867
+ expect(task.dependsOn).toEqual([earlier.id]);
868
+ expect(task.slackTriggerMessageTs).toBe(lastTs!);
869
+ expect(task.task).toContain("2 message(s) buffered");
870
+ expect(task.task).toContain("compare these");
871
+ expect(task.task).not.toContain("not attached");
872
+ expect(await readStoredBytes(task.id)).toEqual([PNG_BYTES, otherBytes]);
873
+ expect(await wasDraft(task.id)).toBe(true);
874
+ expect(await getSteeringMessagesForTask(earlier.id)).toEqual([]);
875
+ expect(failureNotices()).toEqual([]);
876
+ });
877
+
878
+ test(`!now with files flushes existing files without dependency with steering ${steering}`, async () => {
879
+ process.env.SLACK_THREAD_STEERING = steering;
880
+ await sendBuffered("earlier screenshot", [slackFile()]);
881
+ expect(fileRequests).toEqual([]);
882
+ const otherBytes = new Uint8Array([...PNG_BYTES, 4]);
883
+ bytesById.set("F0OTHER0001", otherBytes);
884
+ await sendBuffered("!now", [
885
+ slackFile({ id: "F0OTHER0001", name: "other.png", size: otherBytes.length }),
886
+ ]);
887
+ const task = await followUp();
888
+ expect(task.task).toContain("earlier screenshot");
889
+ expect(task.task).not.toContain("!now");
890
+ expect(task.dependsOn).toEqual([]);
891
+ expect(await getTaskAttachments(task.id)).toHaveLength(2);
892
+ expect(await readStoredBytes(task.id)).toEqual([PNG_BYTES, otherBytes]);
893
+ expect(await getSteeringMessagesForTask(earlier.id)).toEqual([]);
894
+ expect(failureNotices()).toEqual([]);
802
895
  });
803
- } finally {
804
- process.env.ADDITIVE_SLACK = "false";
805
- restoreSteeringEnv();
806
896
  }
807
897
 
808
- expect(fileRequests).toEqual([]);
809
- expect(client.chat.postMessage).toHaveBeenCalledTimes(1);
810
- const [args] = client.chat.postMessage.mock.calls[0] as unknown as [{ text: string }];
811
- expect(args.text).toContain("`screenshot.png`");
812
- const flushed = await getDbClient().get<{ task: string }>(
813
- "SELECT task FROM agent_tasks WHERE slackThreadTs = ? AND task != 'earlier work'",
814
- [threadTs],
815
- );
816
- expect(flushed?.task).toContain("here it is");
817
- expect(flushed?.task).toContain("(not attached: follow-ups queued by ADDITIVE_SLACK");
898
+ test("text-only follow-ups still steer the running task", async () => {
899
+ await sendBuffered("!now use the safer approach");
900
+ const steering = await getSteeringMessagesForTask(earlier.id);
901
+ expect(steering).toHaveLength(1);
902
+ expect(steering[0]!.body).toContain("use the safer approach");
903
+ expect(await getSlackTasksInThread(channelId, threadTs)).toHaveLength(1);
904
+ expect(fileRequests).toEqual([]);
905
+ });
906
+
907
+ test("download failures and oversize files are reported at flush while good files attach", async () => {
908
+ await sendBuffered("inspect these", [
909
+ slackFile(),
910
+ slackFile({
911
+ id: "F0DENIED001",
912
+ name: "denied.png",
913
+ url_private_download: `${slackFiles.url}forbidden`,
914
+ }),
915
+ slackFile({ id: "F0HUGE0001", name: "huge.png", size: HUGE_BYTES }),
916
+ ]);
917
+ expect(failureNotices()).toEqual([]);
918
+ await instantFlush(key);
919
+ const task = await followUp();
920
+ expect(await readStoredBytes(task.id)).toEqual([PNG_BYTES]);
921
+ expect(task.task).toContain("not attached: download failed (HTTP 403)");
922
+ expect(task.task).not.toContain("follow-ups queued by ADDITIVE_SLACK");
923
+ expect(failureNotices()).toHaveLength(1);
924
+ expect(failureNotices()[0]).toContain("`denied.png` (download failed (HTTP 403))");
925
+ expect(failureNotices()[0]).toContain("`huge.png`");
926
+ expect(failureNotices()[0]).not.toContain("`screenshot.png`");
927
+ });
928
+
929
+ test("storage failures are reported after flush", async () => {
930
+ const upload = spyOn(getFileStorageProvider(), "upload").mockRejectedValue(
931
+ new Error("storage unavailable"),
932
+ );
933
+ try {
934
+ await sendBuffered("!now inspect this", [slackFile()]);
935
+ const task = await followUp();
936
+ expect(task.status).toBe("pending");
937
+ expect(await getTaskAttachments(task.id)).toEqual([]);
938
+ expect(failureNotices()).toHaveLength(1);
939
+ expect(failureNotices()[0]).toContain("storage unavailable");
940
+ } finally {
941
+ upload.mockRestore();
942
+ }
943
+ });
944
+
945
+ test("an unavailable Slack app retains the buffered file failure fallback", async () => {
946
+ getApp.mockReturnValue(null);
947
+ await sendBuffered("!now inspect this", [slackFile()]);
948
+ const task = await followUp();
949
+ expect(await getTaskAttachments(task.id)).toEqual([]);
950
+ expect(task.task).toContain("[File: screenshot.png");
951
+ expect(task.task).toContain("not attached: follow-ups queued by ADDITIVE_SLACK");
952
+ expect(await getSteeringMessagesForTask(earlier.id)).toEqual([]);
953
+ });
818
954
  });
819
955
 
820
956
  test("a channel message that mentions the bot with a file attaches it", async () => {
@@ -20,6 +20,7 @@ const ALL_EVENTS: SlackReactionEvent[] = [
20
20
 
21
21
  /** The four acceptance-stage names `main` removed as a fixed list at finalize. */
22
22
  const DEFAULT_ACCEPTANCE_NAMES = ["eyes", "heavy_plus_sign", "zap", "speech_balloon"];
23
+ const SECRET_SHAPED_GITHUB_PAT = `github_pat_${"a".repeat(32)}`;
23
24
 
24
25
  function clearReactionEnv() {
25
26
  for (const key of Object.values(SLACK_REACTION_CONFIG_KEYS)) delete process.env[key];
@@ -320,7 +321,7 @@ describe("reaction-shortcode.ts", () => {
320
321
  test("a generic (non-invalid_name) add failure redacts a secret-shaped error message in the emitted log", async () => {
321
322
  const logSpy = spyOn(console, "log");
322
323
  logSpy.mockClear();
323
- const leaked = "github_pat_11B4WKYAA0Qe95fajGmt3o_ABCDEF1234567890abcdef";
324
+ const leaked = SECRET_SHAPED_GITHUB_PAT;
324
325
  const add = async () => {
325
326
  throw new Error(`rate_limited: ${leaked}`);
326
327
  };
@@ -339,7 +340,7 @@ describe("reaction-shortcode.ts", () => {
339
340
  test("a generic (non-invalid_name/no_reaction) remove failure redacts a secret-shaped error message in the emitted log", async () => {
340
341
  const logSpy = spyOn(console, "log");
341
342
  logSpy.mockClear();
342
- const leaked = "github_pat_11B4WKYAA0Qe95fajGmt3o_ABCDEF1234567890abcdef";
343
+ const leaked = SECRET_SHAPED_GITHUB_PAT;
343
344
  const remove = async () => {
344
345
  const error = new Error(`rate_limited: ${leaked}`) as Error & { data: unknown };
345
346
  error.data = { error: "rate_limited" };
@@ -361,7 +362,7 @@ describe("reaction-shortcode.ts", () => {
361
362
  test("an invalid_name add failure redacts a secret-shaped reaction name in the emitted log", async () => {
362
363
  const errorSpy = spyOn(console, "error");
363
364
  errorSpy.mockClear();
364
- const leaked = "github_pat_11B4WKYAA0Qe95fajGmt3o_ABCDEF1234567890abcdef";
365
+ const leaked = SECRET_SHAPED_GITHUB_PAT;
365
366
  const add = async () => {
366
367
  throw { data: { error: "invalid_name" } };
367
368
  };
@@ -380,7 +381,7 @@ describe("reaction-shortcode.ts", () => {
380
381
  test("a fallback add failure after invalid_name redacts a secret-shaped error message in the emitted log", async () => {
381
382
  const logSpy = spyOn(console, "log");
382
383
  logSpy.mockClear();
383
- const leaked = "github_pat_11B4WKYAA0Qe95fajGmt3o_ABCDEF1234567890abcdef";
384
+ const leaked = SECRET_SHAPED_GITHUB_PAT;
384
385
  const add = async ({ name }: { name: string }) => {
385
386
  if (name === "not_a_real_emoji") throw { data: { error: "invalid_name" } };
386
387
  throw new Error(`rate_limited: ${leaked}`);
@@ -9,7 +9,7 @@ import { mcpOverflowNamespace } from "../tools/utils";
9
9
  import { SKIP_SANDBOX_SPAWN_TESTS } from "./sandbox-spawn-test-helpers";
10
10
 
11
11
  const TEST_DB_PATH = "./test-slack-read-boundaries.sqlite";
12
- const API_KEY = "test-slack-read-boundaries-key-1234567890";
12
+ const API_KEY = "example-test-slack-read-boundaries-key-1234567890";
13
13
  const AGENT_ID = "aaaaaaaa-0000-4000-8000-000000000001";
14
14
  const CHANNEL_ID = "C_BUSY_BOUNDARY";
15
15
  const FULL_MESSAGE_COUNT = 20;
@@ -19,6 +19,7 @@ import {
19
19
  getSlackTreeMessages,
20
20
  getTaskById,
21
21
  initDb,
22
+ insertTaskAttachment,
22
23
  isPendingSlackMessage,
23
24
  markTaskSlackReplySent,
24
25
  startTask,
@@ -39,7 +40,7 @@ import {
39
40
  } from "../slack/render-v2";
40
41
  import { getAgentDisplayName, getAgentEmoji } from "../slack/responses";
41
42
  import { slackContextKey } from "../tasks/context-key";
42
- import type { AgentTask } from "../types";
43
+ import type { AgentTask, TaskAttachment } from "../types";
43
44
  import { clearVolatileSecretsForTesting } from "../utils/secret-scrubber";
44
45
 
45
46
  const TEST_DB_PATH = "./test-slack-render-v2.sqlite";
@@ -1183,6 +1184,95 @@ describe("Slack renderer v2", () => {
1183
1184
  expect(calls.some((call) => call.method === "chat.appendStream")).toBe(false);
1184
1185
  });
1185
1186
 
1187
+ test.each<{
1188
+ label: string;
1189
+ attachment: Partial<TaskAttachment> & Pick<TaskAttachment, "kind" | "name">;
1190
+ expected: string;
1191
+ }>([
1192
+ {
1193
+ label: "plain ASCII positive control",
1194
+ attachment: { kind: "agent-fs", name: "report.txt", path: "/reports/report.txt" },
1195
+ expected:
1196
+ "📎 [report.txt](https://files.example.test/file/~/org-1/drive-1/reports/report.txt)",
1197
+ },
1198
+ {
1199
+ label: "raw path spaces and parentheses",
1200
+ attachment: { kind: "agent-fs", name: "final report", path: "/shared reports/final (v1).md" },
1201
+ expected:
1202
+ "📎 [final report](https://files.example.test/file/~/org-1/drive-1/shared%20reports/final%20%28v1%29.md)",
1203
+ },
1204
+ {
1205
+ label: "already encoded positive control",
1206
+ attachment: { kind: "agent-fs", name: "report", path: "/reports/final%20%28v1%29.md" },
1207
+ expected:
1208
+ "📎 [report](https://files.example.test/file/~/org-1/drive-1/reports/final%20%28v1%29.md)",
1209
+ },
1210
+ {
1211
+ label: "label delimiters, backslashes, and whitespace",
1212
+ attachment: {
1213
+ kind: "url",
1214
+ name: " \t[report] (final)\\\r\n\t copy ",
1215
+ url: "https://example.test/report",
1216
+ },
1217
+ expected: "📎 [\\[report\\] \\(final\\)\\\\ copy](https://example.test/report)",
1218
+ },
1219
+ {
1220
+ label: "URL positive control with an IPv6 host",
1221
+ attachment: { kind: "url", name: "report", url: "http://[::1]/report.txt?q=a%20b&x=1#part" },
1222
+ expected: "📎 [report](http://[::1]/report.txt?q=a%20b&x=1#part)",
1223
+ },
1224
+ {
1225
+ label: "URL delimiters with existing escapes and query parameters",
1226
+ attachment: {
1227
+ kind: "url",
1228
+ name: "report",
1229
+ url: "https://example.test/a%20b) [c]<(d)>\\file?q=one two&x=1#part",
1230
+ },
1231
+ expected:
1232
+ "📎 [report](https://example.test/a%20b%29%20[c]%3C%28d%29%3E%5Cfile?q=one%20two&x=1#part)",
1233
+ },
1234
+ {
1235
+ label: "non-HTTP fallback stays omitted",
1236
+ attachment: { kind: "url", name: "report", url: "agent-fs:/reports/report.md" },
1237
+ expected: "",
1238
+ },
1239
+ ])("streams safe attachment Markdown: $label", async ({ attachment, expected }) => {
1240
+ const originalHost = process.env.AGENT_FS_LIVE_URL;
1241
+ process.env.AGENT_FS_LIVE_URL = "https://files.example.test";
1242
+ try {
1243
+ const lead = await createAgent({ name: "Attachment Lead", isLead: true, status: "idle" });
1244
+ const { channelId, threadTs } = uniqueSlackAddress("C_OUTCOME_ATTACHMENT");
1245
+ const ask = await createTaskExtended("render attachment link", {
1246
+ agentId: lead.id,
1247
+ source: "slack",
1248
+ slackChannelId: channelId,
1249
+ slackThreadTs: threadTs,
1250
+ contextKey: slackContextKey({ channelId, threadTs }),
1251
+ });
1252
+ await insertTaskAttachment({
1253
+ ...attachment,
1254
+ taskId: ask.id,
1255
+ agentId: lead.id,
1256
+ orgId: "org-1",
1257
+ driveId: "drive-1",
1258
+ isPrimary: true,
1259
+ });
1260
+ await startTask(ask.id);
1261
+ await ensureSlackThreadTree([ask.id]);
1262
+ await completeTask(ask.id, "Done");
1263
+ calls.length = 0;
1264
+ _resetSlackRenderV2ForTests();
1265
+
1266
+ await processSlackRenderV2();
1267
+
1268
+ const started = calls.find((call) => call.method === "chat.startStream");
1269
+ expect(started?.payload.markdown_text).toBe(`✅\n\nDone${expected ? `\n\n${expected}` : ""}`);
1270
+ } finally {
1271
+ if (originalHost === undefined) delete process.env.AGENT_FS_LIVE_URL;
1272
+ else process.env.AGENT_FS_LIVE_URL = originalHost;
1273
+ }
1274
+ });
1275
+
1186
1276
  test("truncates oversized Markdown before a code fence and links the full task", async () => {
1187
1277
  const lead = await createAgent({ name: "Overflow Lead", isLead: true, status: "idle" });
1188
1278
  const { channelId, threadTs } = uniqueSlackAddress("C_OUTCOME_OVERFLOW");
@@ -1626,6 +1716,35 @@ describe("Slack renderer v2", () => {
1626
1716
  expect(outcome?.finalizedAt).toBeDefined();
1627
1717
  });
1628
1718
 
1719
+ test("keeps a deferral's human-facing line even when the agent already sent a slack-reply", async () => {
1720
+ const lead = await createAgent({ name: "Deferring Lead", isLead: true, status: "idle" });
1721
+ const { channelId, threadTs } = uniqueSlackAddress("C_RENDER_DEFER_AFTER_REPLY");
1722
+ const ask = await createTaskExtended("defer after replying by hand", {
1723
+ agentId: lead.id,
1724
+ source: "slack",
1725
+ slackChannelId: channelId,
1726
+ slackThreadTs: threadTs,
1727
+ contextKey: slackContextKey({ channelId, threadTs }),
1728
+ });
1729
+ await startTask(ask.id);
1730
+ await ensureSlackThreadTree([ask.id]);
1731
+ // Agent posts a hand-written slack-reply first (e.g. a transcription),
1732
+ // then defer-task completes the task with the engine-authored deferral
1733
+ // line and tags it "deferred" (mirrors defer-task.ts's completeTask call).
1734
+ await markTaskSlackReplySent(ask.id);
1735
+ const deferralLine =
1736
+ "Deferred until today 17:30:19 UTC ([715bf847](https://app.agent-swarm.dev/schedules/715bf847-fe3e-40e9-9fef-3297a62d9afd)) -> checking the new defer card";
1737
+ await completeTask(ask.id, deferralLine, { addTags: ["deferred"] });
1738
+ calls.length = 0;
1739
+ _resetSlackRenderV2ForTests();
1740
+
1741
+ await processSlackRenderV2();
1742
+
1743
+ const started = calls.find((call) => call.method === "chat.startStream");
1744
+ expect(started?.payload.markdown_text).toBe(`✅\n\n${deferralLine}`);
1745
+ expect(started?.payload.markdown_text).not.toBe(`✅ ${lead.name} completed`);
1746
+ });
1747
+
1629
1748
  test("refreshes a stream started with stale content before finalizing it", async () => {
1630
1749
  const lead = await createAgent({ name: "Refresh Lead", isLead: true, status: "idle" });
1631
1750
  const { channelId, threadTs } = uniqueSlackAddress("C_RENDER_REFRESH_STALE");