@desplega.ai/agent-swarm 1.152.2 → 1.154.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 (284) hide show
  1. package/README.md +2 -2
  2. package/dist/{acp-adapter-pfeje2v5.js → acp-adapter-eb2aj8xy.js} +4 -4
  3. package/dist/{actions-0sttc7f5.js → actions-kpnwp1kz.js} +31 -27
  4. package/dist/{anthropic-messages-2y8nktar.js → anthropic-messages-9vqsbznt.js} +112 -84
  5. package/dist/{app-p8teva95.js → app-zk1apkf6.js} +10 -7
  6. package/dist/{assistant-r45jnsrt.js → assistant-kwq4y50q.js} +32 -28
  7. package/dist/{azure-openai-responses-f38x2avq.js → azure-openai-responses-04qwdwfq.js} +27 -13
  8. package/dist/{boot-reembed-je26h5y7.js → boot-reembed-22hd3jjn.js} +9 -7
  9. package/dist/{boot-reembed-1qmybsd4.js → boot-reembed-qe168424.js} +10 -8
  10. package/dist/{boot-scrub-logs-gte3h1mm.js → boot-scrub-logs-pkdpsrjd.js} +7 -5
  11. package/dist/{claude-adapter-csc1mzka.js → claude-adapter-3fx90nww.js} +18 -16
  12. package/dist/{claude-managed-adapter-dsmkcrjy.js → claude-managed-adapter-ea2aga6w.js} +3 -3
  13. package/dist/{claude-sdk-session-fcd6f078.js → claude-sdk-session-z7fahf1k.js} +18 -16
  14. package/dist/{cli-r2fpe7cx.js → cli-00w2c66p.js} +61 -55
  15. package/dist/{cli-wbyk465z.js → cli-09y3tjxz.js} +1 -1
  16. package/dist/{cli-ht5rs63n.js → cli-0e31nm4n.js} +16 -7
  17. package/dist/cli-0x5qzvkg.js +4279 -0
  18. package/dist/{cli-d3p82erb.js → cli-18amys3y.js} +9 -9
  19. package/dist/{cli-4ahpr4sw.js → cli-1jpjafw4.js} +5 -5
  20. package/dist/{cli-jbmc0r7r.js → cli-243tzws8.js} +1 -1
  21. package/dist/{cli-ch38q5z4.js → cli-24k806sc.js} +6789 -2542
  22. package/dist/cli-3bkz2p7j.js +2828 -0
  23. package/dist/cli-3er6gcsn.js +1849 -0
  24. package/dist/cli-4k7jazkq.js +3339 -0
  25. package/dist/{cli-c4eeb5q2.js → cli-5b6b62tz.js} +1 -1
  26. package/dist/{cli-qd65dvr6.js → cli-5vs0nh43.js} +65 -44
  27. package/dist/{cli-z3wkng35.js → cli-6r3cgpk2.js} +224 -156
  28. package/dist/{cli-pgj4a5fb.js → cli-71cj99hx.js} +4 -3
  29. package/dist/cli-8sea27q4.js +14075 -0
  30. package/dist/{cli-jvy9qgjs.js → cli-913yakhv.js} +22 -7
  31. package/dist/{cli-pf9q3696.js → cli-927erqqt.js} +5 -2
  32. package/dist/cli-95jnpnqg.js +230 -0
  33. package/dist/{cli-wpv5rgg2.js → cli-abp3g4ax.js} +15 -2
  34. package/dist/{cli-ckz5t2hb.js → cli-acy6bfhr.js} +10 -7
  35. package/dist/{cli-4yc5d8fk.js → cli-avkd6mqv.js} +50 -0
  36. package/dist/{cli-8wrympd7.js → cli-bzr0ftht.js} +26 -7
  37. package/dist/{cli-qn042eh7.js → cli-c1v10xb2.js} +194 -1913
  38. package/dist/cli-cgj4hc12.js +18902 -0
  39. package/dist/{cli-2nnd56zt.js → cli-e0pg4zeh.js} +3 -2
  40. package/dist/{cli-r84ctdxn.js → cli-e31ehqwy.js} +2 -2
  41. package/dist/{cli-gphgnvpf.js → cli-e3229na9.js} +3 -3
  42. package/dist/{cli-gpqcs935.js → cli-e3cyf59d.js} +2 -2
  43. package/dist/{cli-s25avqg9.js → cli-e781c6f9.js} +1 -0
  44. package/dist/{cli-s2qjdd6z.js → cli-eetrvv91.js} +1 -1
  45. package/dist/{cli-j4nv2wqy.js → cli-fdzg6jhf.js} +10 -4
  46. package/dist/{cli-fm4ytsdk.js → cli-g2h29qar.js} +2 -2
  47. package/dist/{cli-zvgj5zg8.js → cli-ge67p080.js} +302 -3497
  48. package/dist/cli-h38x0qhg.js +6912 -0
  49. package/dist/{cli-9xvdm3bx.js → cli-h60qfasa.js} +742 -278
  50. package/dist/{cli-jew3jda5.js → cli-hws0vn86.js} +2 -2
  51. package/dist/{cli-j8dggbcr.js → cli-k0g1sjzm.js} +23 -31
  52. package/dist/{cli-xw7vak96.js → cli-kp3qs53m.js} +5 -5
  53. package/dist/{cli-rgvp6r2x.js → cli-mcnhgnn1.js} +193 -4
  54. package/dist/{cli-zvkq5csn.js → cli-myaxtgh8.js} +7 -6
  55. package/dist/{cli-56h18ng2.js → cli-n62atwh9.js} +1 -1
  56. package/dist/cli-n7h1kqpf.js +29 -0
  57. package/dist/{cli-fmy65jdn.js → cli-pf1v8xhb.js} +7 -5
  58. package/dist/{cli-xy1cchaf.js → cli-q85qhckh.js} +238 -11
  59. package/dist/{cli-3bpzj8me.js → cli-r1d3dzt6.js} +2 -2
  60. package/dist/{pi-mono-adapter-jcqt71cg.js → cli-rxkqvngf.js} +67343 -101083
  61. package/dist/{cli-kj76p1ez.js → cli-scwq51gn.js} +13 -7
  62. package/dist/{cli-b9dd0h3k.js → cli-stwsmd6k.js} +3 -2
  63. package/dist/{cli-c2mk1kgf.js → cli-v4en75c5.js} +8 -1
  64. package/dist/{cli-z61ykvam.js → cli-wg04s9kr.js} +4 -3
  65. package/dist/cli-yn0a417a.js +4069 -0
  66. package/dist/{cli-zfzznn7c.js → cli-ytxs7mn1.js} +26 -2
  67. package/dist/{cli-4j8k1m4w.js → cli-zt1227k1.js} +1162 -192
  68. package/dist/cli.js +28 -35
  69. package/dist/{codex-adapter-fxmpsabw.js → codex-adapter-ttdm995v.js} +12 -11
  70. package/dist/{codex-hook-8jexc34x.js → codex-hook-8gx7bcz0.js} +3 -3
  71. package/dist/{codex-session-runner-rr1e47ea.js → codex-session-runner-3v8nh4c8.js} +12 -11
  72. package/dist/{commands-0f5rxz51.js → commands-1cxwbhhq.js} +7 -5
  73. package/dist/{db-r3jxf3wy.js → db-y9zwsr79.js} +25 -5
  74. package/dist/{dispatcher-06vph6q4.js → dispatcher-9m9zss36.js} +30 -26
  75. package/dist/dsh-adapter-gsb2qqf9.js +19 -0
  76. package/dist/{e2b-413b8t4v.js → e2b-93x1k0j5.js} +6 -7
  77. package/dist/{google-generative-ai-ndjmj5fa.js → google-generative-ai-gwwrnzqn.js} +33 -73
  78. package/dist/{google-vertex-fr665tqs.js → google-vertex-5wfawpwz.js} +38 -68
  79. package/dist/{handlers-0hgtc910.js → handlers-36fm1bwn.js} +33 -29
  80. package/dist/{hook-9kvrv1er.js → hook-pntw4r8h.js} +12 -10
  81. package/dist/{hook-fbfhkat3.js → hook-vwq8y26p.js} +13 -11
  82. package/dist/{http-xbyx8k5j.js → http-20jesakk.js} +357 -261
  83. package/dist/{index-jsw3aynb.js → index-6artrehg.js} +30 -26
  84. package/dist/{cli-pdz9qde4.js → index-9ch1n761.js} +10 -11
  85. package/dist/{index-rnyahfn5.js → index-9n5z2492.js} +30 -26
  86. package/dist/{index-66xz5e9j.js → index-h5gs261z.js} +521 -9
  87. package/dist/{index-tfsdxyv6.js → index-j5wrf1yw.js} +40 -25
  88. package/dist/{index-2sp0384z.js → index-kkeftv5h.js} +738 -21
  89. package/dist/{index-sgk50mbg.js → index-m2xqpz5r.js} +31 -27
  90. package/dist/{index-gpfn1741.js → index-m9ks8a7t.js} +9 -7
  91. package/dist/index-w097eg0x.js +5 -0
  92. package/dist/jiti-loader-b4837mtm.js +34 -0
  93. package/dist/jiti-static-loader-q2g5ds0y.js +31267 -0
  94. package/dist/{keepalive-s3xpr7sw.js → keepalive-pwxvxd2c.js} +9 -7
  95. package/dist/{lead-rq4n5r16.js → lead-m789wqa3.js} +46 -41
  96. package/dist/{maintenance-f0atj1ab.js → maintenance-bvg6w7e2.js} +10 -8
  97. package/dist/{mistral-conversations-qhr84ph3.js → mistral-conversations-zx0kn554.js} +24 -18
  98. package/dist/{node-x1g4v0pq.js → node-5qtycf6a.js} +16 -11
  99. package/dist/{oauth-refresh-sweep-q0t3bxre.js → oauth-refresh-sweep-6v4bd737.js} +8 -6
  100. package/dist/{onboard-br5j1kch.js → onboard-snckpcj1.js} +115 -35
  101. package/dist/{openai-codex-responses-wy6a43wd.js → openai-codex-responses-4vjqzvjp.js} +33 -27
  102. package/dist/{openai-completions-32rdwmze.js → openai-completions-pxsmym7g.js} +52 -64
  103. package/dist/{openai-responses-6xwf7bzg.js → openai-responses-tmn0rqp0.js} +23 -24
  104. package/dist/{opencode-adapter-28axqa09.js → opencode-adapter-qbmv4tpy.js} +6 -6
  105. package/dist/{otel-impl-vdqge2nt.js → otel-impl-e0ytens5.js} +4 -4
  106. package/dist/{pi-messages-b1mbr6gp.js → pi-messages-txpjj39q.js} +3 -3
  107. package/dist/pi-mono-adapter-c4ggemhz.js +1316 -0
  108. package/dist/{pricing-refresh-hb3691fd.js → pricing-refresh-52g2ywrw.js} +9 -7
  109. package/dist/{rbac-roles-t78xt39b.js → rbac-roles-5455kfdn.js} +8 -6
  110. package/dist/{rbac-roles-jh280k9g.js → rbac-roles-hbf0xa52.js} +9 -7
  111. package/dist/{render-v2-7p8r2ymg.js → render-v2-npm8xdzh.js} +16 -9
  112. package/dist/{seed-pricing-hf889enh.js → seed-pricing-650wbr1n.js} +8 -6
  113. package/dist/{setup-w6mb03jv.js → setup-k1mew5rf.js} +4 -5
  114. package/dist/virtual-modules-2q5b2pk4.js +54 -0
  115. package/dist/{work-objects-nt8gy1kj.js → work-objects-2ea3kvwg.js} +33 -29
  116. package/dist/{worker-dwy617t0.js → worker-ec6hvc4m.js} +46 -41
  117. package/openapi.json +109 -16
  118. package/package.json +7 -6
  119. package/plugin/opencode-plugins/lib/memory-raters.ts +7 -0
  120. package/src/be/automation-preflight.ts +6 -4
  121. package/src/be/db/agents.ts +75 -2
  122. package/src/be/db/tasks/read.ts +2 -0
  123. package/src/be/db/tasks/write.ts +7 -1
  124. package/src/be/db.ts +140 -18
  125. package/src/be/extensions/db.ts +35 -4
  126. package/src/be/memory/raters/llm.ts +2 -6
  127. package/src/be/memory/raters/noop.ts +2 -3
  128. package/src/be/memory/raters/registry.ts +4 -10
  129. package/src/be/migrations/157_defer_provenance_and_marker.sql +27 -0
  130. package/src/be/models-catalog.ts +2 -0
  131. package/src/be/modelsdev-cache.json +213 -0
  132. package/src/be/rbac-audit.ts +2 -0
  133. package/src/be/seed/agent-fs-provision.ts +23 -0
  134. package/src/be/seed-pricing.ts +3 -1
  135. package/src/be/swarm-config-guard.ts +1 -0
  136. package/src/commands/onboard/compose-generator.ts +16 -1
  137. package/src/commands/onboard/env-generator.ts +14 -1
  138. package/src/commands/onboard/manifest.ts +1 -0
  139. package/src/commands/onboard/steps/integration-slack.tsx +56 -16
  140. package/src/commands/onboard/steps/review.tsx +5 -0
  141. package/src/commands/onboard/types.ts +6 -0
  142. package/src/commands/onboard.tsx +5 -1
  143. package/src/commands/provider-credentials.ts +14 -2
  144. package/src/commands/runner.ts +18 -2
  145. package/src/extensions/identity.ts +15 -7
  146. package/src/extensions/lifecycle.ts +15 -21
  147. package/src/hooks/codex-hook.ts +7 -1
  148. package/src/http/agents.ts +12 -4
  149. package/src/http/config.ts +1 -1
  150. package/src/http/core.ts +5 -3
  151. package/src/http/extensions.ts +108 -34
  152. package/src/http/poll.ts +7 -0
  153. package/src/http/schedules.ts +10 -0
  154. package/src/http/script-runs.ts +35 -26
  155. package/src/http/status.ts +24 -11
  156. package/src/http/tasks.ts +6 -1
  157. package/src/prompts/base-prompt.ts +15 -7
  158. package/src/prompts/memories.ts +5 -7
  159. package/src/prompts/session-templates.ts +3 -2
  160. package/src/prompts/steering-delivery.ts +2 -0
  161. package/src/providers/claude-managed-models.ts +8 -0
  162. package/src/providers/codex-adapter.ts +3 -6
  163. package/src/providers/codex-models.ts +15 -1
  164. package/src/providers/dsh-adapter.ts +248 -0
  165. package/src/providers/index.ts +5 -1
  166. package/src/providers/modelsdev-reasoning.json +50 -0
  167. package/src/providers/pi-mono-extension.ts +2 -5
  168. package/src/rbac/legacy-policy.ts +15 -1
  169. package/src/rbac/types.ts +2 -0
  170. package/src/server.ts +7 -1
  171. package/src/slack/ack.ts +43 -8
  172. package/src/slack/app.ts +32 -10
  173. package/src/slack/config.ts +29 -0
  174. package/src/slack/handlers.ts +7 -1
  175. package/src/slack/render-v2.ts +186 -17
  176. package/src/slack/responses.ts +1 -3
  177. package/src/slack/task-output.ts +66 -8
  178. package/src/slack/thread-buffer.ts +27 -3
  179. package/src/telemetry.ts +2 -3
  180. package/src/tests/agent-fs-provision-seeder.test.ts +56 -2
  181. package/src/tests/agents-harness-provider.test.ts +19 -0
  182. package/src/tests/agents-list-model-display.test.ts +60 -1
  183. package/src/tests/base-prompt.test.ts +25 -0
  184. package/src/tests/claude-managed-adapter.test.ts +6 -0
  185. package/src/tests/codex-gpt6-release.test.ts +97 -0
  186. package/src/tests/codex-steering-hook.test.ts +2 -2
  187. package/src/tests/config-api-only-keys.test.ts +9 -0
  188. package/src/tests/context-window.test.ts +1 -0
  189. package/src/tests/db-task-read-contract.test.ts +9 -0
  190. package/src/tests/db-task-write-contract.test.ts +9 -0
  191. package/src/tests/defer-task.test.ts +115 -16
  192. package/src/tests/dsh-adapter.test.ts +276 -0
  193. package/src/tests/e2e-nightly-report.test.ts +3 -3
  194. package/src/tests/e2e-redact.test.ts +2 -0
  195. package/src/tests/env-flag.test.ts +6 -0
  196. package/src/tests/extension-agent-visibility.test.ts +474 -0
  197. package/src/tests/extensions-db.test.ts +41 -0
  198. package/src/tests/extensions-http.test.ts +148 -4
  199. package/src/tests/extensions-lifecycle.test.ts +21 -1
  200. package/src/tests/extensions-mcp-tools.test.ts +9 -5
  201. package/src/tests/fetch-resolved-env.test.ts +43 -0
  202. package/src/tests/internal-ai/complete-structured.test.ts +32 -9
  203. package/src/tests/internal-ai/credentials.test.ts +4 -4
  204. package/src/tests/internal-ai/models.test.ts +20 -0
  205. package/src/tests/memory-rate-tool.test.ts +7 -10
  206. package/src/tests/memory-rater-defaults.test.ts +23 -0
  207. package/src/tests/memory-rater-e2e.test.ts +2 -3
  208. package/src/tests/migration-046-budgets.test.ts +2 -0
  209. package/src/tests/model-control.test.ts +2 -2
  210. package/src/tests/onboard-compose.test.ts +14 -0
  211. package/src/tests/onboard-env.test.ts +17 -0
  212. package/src/tests/onboard-manifest.test.ts +12 -0
  213. package/src/tests/opus-5-5-registry.test.ts +48 -0
  214. package/src/tests/pricing-refresh.test.ts +1 -1
  215. package/src/tests/pricing-routes.test.ts +15 -3
  216. package/src/tests/provider-registration.test.ts +5 -2
  217. package/src/tests/rbac-admission-e2e.test.ts +54 -0
  218. package/src/tests/rbac-e2e-helpers.ts +2 -0
  219. package/src/tests/rbac-engine.test.ts +32 -0
  220. package/src/tests/rbac-wire-e2e.test.ts +33 -0
  221. package/src/tests/reasoning-effort.test.ts +8 -0
  222. package/src/tests/reload-config.test.ts +33 -0
  223. package/src/tests/script-connections.test.ts +3 -0
  224. package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
  225. package/src/tests/slack-config.test.ts +136 -0
  226. package/src/tests/slack-mode-lifecycle.test.ts +69 -0
  227. package/src/tests/slack-render-v2.test.ts +446 -16
  228. package/src/tests/slack-steering.test.ts +69 -1
  229. package/src/tests/slack-task-output.test.ts +33 -12
  230. package/src/tests/status.test.ts +22 -2
  231. package/src/tests/steering-core.test.ts +35 -0
  232. package/src/tests/steering-delivery.test.ts +39 -0
  233. package/src/tests/steering-transport.test.ts +2 -2
  234. package/src/tests/task-tool-preload-mcp.test.ts +3 -1
  235. package/src/tests/telemetry-init.test.ts +18 -0
  236. package/src/tests/workflow-live-recovery.test.ts +126 -0
  237. package/src/tests/workflow-llm.test.ts +3 -3
  238. package/src/tools/defer-task.ts +64 -26
  239. package/src/tools/extension-common.ts +4 -0
  240. package/src/tools/extension-install.ts +1 -1
  241. package/src/tools/extension-list.ts +4 -3
  242. package/src/tools/join-swarm.ts +1 -1
  243. package/src/tools/schedules/create-schedule.ts +11 -1
  244. package/src/tools/schedules/patch-schedule.ts +5 -0
  245. package/src/tools/schedules/update-schedule.ts +5 -0
  246. package/src/tools/send-task.ts +6 -0
  247. package/src/tools/utils.ts +1 -1
  248. package/src/types.ts +30 -8
  249. package/src/utils/context-window.ts +1 -0
  250. package/src/utils/credentials.ts +8 -0
  251. package/src/utils/internal-ai/complete-structured.ts +8 -0
  252. package/src/utils/internal-ai/models.ts +5 -5
  253. package/src/utils/memory-raters.ts +3 -0
  254. package/src/workflows/engine.ts +28 -0
  255. package/src/workflows/recovery.ts +7 -4
  256. package/templates/schedules/daily-status-report/content.md +25 -12
  257. package/templates/schedules/daily-workflow-health-audit/content.md +1 -1
  258. package/templates/skills/swarm-extensions/content.md +9 -5
  259. package/dist/cli-4wppfkcm.js +0 -16384
  260. package/dist/cli-55qrx3q8.js +0 -6041
  261. package/dist/cli-cg4vnyh4.js +0 -305
  262. package/dist/cli-jyetr64j.js +0 -52
  263. package/dist/cli-pp9ax9pa.js +0 -520
  264. package/dist/cli-qa66qaf0.js +0 -6076
  265. package/dist/cli-qghpvca2.js +0 -590
  266. package/dist/cli-rembwgt8.js +0 -861
  267. package/dist/cli-tcvj7j2r.js +0 -1971
  268. package/dist/cli-vfcth11k.js +0 -36
  269. package/dist/cli-w1d1bgkk.js +0 -1422
  270. package/dist/cli-y6x0gqtg.js +0 -5977
  271. package/dist/es5-tj3tn4jj.js +0 -844
  272. package/dist/index-1kq5hw2p.js +0 -467
  273. package/dist/index-1p8wdstt.js +0 -7
  274. package/dist/index-8fdjzb2f.js +0 -100
  275. package/dist/index-bxmwzbgw.js +0 -365
  276. package/dist/index-nzerxd5z.js +0 -7
  277. package/dist/index-pyymnzat.js +0 -1168
  278. package/dist/index-qv6atbab.js +0 -619
  279. package/dist/index-rja32qa7.js +0 -801
  280. package/dist/index-tzg5srfp.js +0 -211
  281. package/dist/index-vhncw983.js +0 -90
  282. package/dist/index-xk036zsk.js +0 -859
  283. package/dist/index-zyazbzcb.js +0 -6
  284. package/dist/{claude-managed-setup-48dwg9bv.js → claude-managed-setup-f5pmsyrp.js} +3 -3
@@ -1,10 +1,11 @@
1
- import { afterAll, beforeAll, describe, expect, test } from "bun:test";
1
+ import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
2
2
  import { unlinkSync } from "node:fs";
3
3
  import {
4
4
  closeDb,
5
5
  completeTask,
6
6
  createAgent,
7
7
  createTaskExtended,
8
+ createUser,
8
9
  getChildTasks,
9
10
  getLatestActiveTaskInThread,
10
11
  getLogsByTaskId,
@@ -15,6 +16,8 @@ import {
15
16
 
16
17
  process.env.SLACK_RENDER_V2 = "false";
17
18
 
19
+ import { linkIdentity } from "../be/users";
20
+ import * as slackApp from "../slack/app";
18
21
  import { buildTreeBlocks } from "../slack/blocks";
19
22
  import { routeMessage } from "../slack/router";
20
23
  import { formatSlackSteeringAck, requestSlackThreadSteering } from "../slack/steering";
@@ -37,6 +40,7 @@ async function createRunningSlackTask(
37
40
  channelId: string,
38
41
  threadTs: string,
39
42
  task = "existing Slack task",
43
+ requestedByUserId?: string,
40
44
  ) {
41
45
  const created = await createTaskExtended(task, {
42
46
  agentId,
@@ -44,6 +48,7 @@ async function createRunningSlackTask(
44
48
  slackChannelId: channelId,
45
49
  slackThreadTs: threadTs,
46
50
  slackUserId: "U_REQUESTER",
51
+ requestedByUserId,
47
52
  });
48
53
  const started = await startTask(created.id);
49
54
  expect(started?.status).toBe("in_progress");
@@ -220,6 +225,69 @@ describe("Slack thread steering", () => {
220
225
  expect(await getChildTasks(leadTask.id)).toEqual([]);
221
226
  });
222
227
 
228
+ test("buffered replies resolve registered senders and keep mixed authors separate", async () => {
229
+ process.env.SLACK_THREAD_STEERING = "lead";
230
+ process.env.SLACK_THREAD_STEERING_MODE = "queue";
231
+ const channelId = "C_STEER_SENDERS";
232
+ const threadTs = "5100.0001";
233
+ const taras = await createUser({ name: "Taras", email: "taras-steering@example.com" });
234
+ const other = await createUser({ name: "Other", email: "other-steering@example.com" });
235
+ const task = await createRunningSlackTask(leadId, channelId, threadTs, undefined, other.id);
236
+ await linkIdentity(other.id, "slack", "U_OTHER_STEER", { kind: "system", id: "test" });
237
+ // Taras has no Slack alias yet: exercise the same email cascade as task creation.
238
+ const infoUsers: string[] = [];
239
+ const appSpy = spyOn(slackApp, "getSlackApp").mockReturnValue({
240
+ client: {
241
+ users: {
242
+ info: async ({ user }: { user: string }) => {
243
+ infoUsers.push(user);
244
+ return {
245
+ user: { profile: user === "U_TARAS_STEER" ? { email: taras.email } : {} },
246
+ };
247
+ },
248
+ },
249
+ chat: { postMessage: async () => ({ ok: true }) },
250
+ },
251
+ } as unknown as NonNullable<ReturnType<typeof slackApp.getSlackApp>>);
252
+ try {
253
+ bufferThreadMessage(channelId, threadTs, "first", "U_TARAS_STEER", "5100.0002");
254
+ bufferThreadMessage(channelId, threadTs, "second", "U_TARAS_STEER", "5100.0003");
255
+ bufferThreadMessage(channelId, threadTs, "other correction", "U_OTHER_STEER", "5100.0004");
256
+ bufferThreadMessage(
257
+ channelId,
258
+ threadTs,
259
+ "unmapped correction",
260
+ "U_UNKNOWN_STEER",
261
+ "5100.0005",
262
+ );
263
+ bufferThreadMessage(channelId, threadTs, "last", "U_TARAS_STEER", "5100.0006");
264
+ await instantFlush(`${channelId}:${threadTs}`);
265
+
266
+ const messages = await getSteeringMessagesForTask(task.id);
267
+ expect(messages).toHaveLength(4);
268
+ for (const [body, userId, label] of [
269
+ ["first\n---\nsecond", taras.id, "Taras (user)"],
270
+ ["other correction", other.id, "Other (user)"],
271
+ ["unmapped correction", undefined, "Unknown (user)"],
272
+ ["last", taras.id, "Taras (user)"],
273
+ ]) {
274
+ expect(messages.find((message) => message.body === body)).toMatchObject({
275
+ source: "slack",
276
+ createdByKind: "user",
277
+ createdByUserId: userId,
278
+ senderLabel: label,
279
+ });
280
+ }
281
+ expect(infoUsers).toEqual(["U_TARAS_STEER", "U_UNKNOWN_STEER"]);
282
+ expect(
283
+ (await getLogsByTaskId(task.id)).filter((log) => log.newValue === "slack_reaction"),
284
+ ).toHaveLength(5);
285
+ expect(await getChildTasks(task.id)).toEqual([]);
286
+ } finally {
287
+ appSpy.mockRestore();
288
+ }
289
+ });
290
+
223
291
  test("all mode targets the latest active task", async () => {
224
292
  process.env.SLACK_THREAD_STEERING = "all";
225
293
  const channelId = "C_STEER_ALL";
@@ -1,25 +1,46 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { slackTaskOutput } from "../slack/task-output";
3
3
 
4
+ /** The shape `defer-task` writes today: the human card, already rendered. */
4
5
  const task = {
5
6
  status: "completed" as const,
6
7
  tags: ["deferred"],
8
+ output: "Waiting on Researcher — or tomorrow at 17:30 at the latest",
9
+ };
10
+
11
+ /** The shape written before defer-task stored the card directly. */
12
+ const legacy = {
13
+ ...task,
7
14
  output:
8
15
  "Deferred until tomorrow 17:30 UTC ([12345678](https://example.com/schedules/12345678)) -> check the build",
9
16
  };
10
17
 
11
18
  describe("Slack deferral output", () => {
12
- test("keeps the pending work without the ETA or schedule link", () => {
13
- expect(slackTaskOutput(task)).toBe("Pending: check the build");
14
- expect(task.output).toContain("tomorrow 17:30 UTC");
19
+ test("posts the stored deferral card verbatim", () => {
20
+ expect(slackTaskOutput(task)).toBe(
21
+ "Waiting on Researcher — or tomorrow at 17:30 at the latest",
22
+ );
23
+ expect(slackTaskOutput({ ...task, output: "Checking back today at 00:31" })).toBe(
24
+ "Checking back today at 00:31",
25
+ );
15
26
  });
16
27
 
17
- test("removes an ETA-only line and preserves punctuation in the pending description", () => {
18
- expect(slackTaskOutput({ ...task, output: task.output.replace("check the build", "") })).toBe(
19
- "",
20
- );
21
- expect(slackTaskOutput({ ...task, output: `${task.output} (CI) -> deploy` })).toBe(
22
- "Pending: check the build (CI) -> deploy",
28
+ test("rewrites a legacy notice to its ETA, dropping the note and the schedule link", () => {
29
+ const rendered = slackTaskOutput(legacy);
30
+ expect(rendered).toBe("Checking back tomorrow 17:30 UTC");
31
+ expect(rendered).not.toContain("check the build");
32
+ expect(rendered).not.toContain("schedules/12345678");
33
+ });
34
+
35
+ test("a legacy notice with no pending text still yields its ETA, not an empty card", () => {
36
+ expect(
37
+ slackTaskOutput({ ...legacy, output: legacy.output.replace("check the build", "") }),
38
+ ).toBe("Checking back tomorrow 17:30 UTC");
39
+ });
40
+
41
+ test("an arrow inside a legacy note does not extend the ETA", () => {
42
+ expect(slackTaskOutput({ ...legacy, output: `${legacy.output} (CI) -> deploy` })).toBe(
43
+ "Checking back tomorrow 17:30 UTC",
23
44
  );
24
45
  });
25
46
 
@@ -28,9 +49,9 @@ describe("Slack deferral output", () => {
28
49
  });
29
50
 
30
51
  test("preserves schema output and non-deferral output verbatim", () => {
31
- expect(slackTaskOutput({ ...task, outputSchema: { type: "string" } })).toBe(task.output);
32
- expect(slackTaskOutput({ ...task, tags: [] })).toBe(task.output);
33
- expect(slackTaskOutput({ ...task, status: "in_progress" })).toBe(task.output);
52
+ expect(slackTaskOutput({ ...legacy, outputSchema: { type: "string" } })).toBe(legacy.output);
53
+ expect(slackTaskOutput({ ...legacy, tags: [] })).toBe(legacy.output);
54
+ expect(slackTaskOutput({ ...legacy, status: "in_progress" })).toBe(legacy.output);
34
55
  expect(slackTaskOutput({ ...task, output: undefined })).toBeUndefined();
35
56
  });
36
57
  });
@@ -96,6 +96,8 @@ const ENV_KEYS_TO_RESET = [
96
96
  "DEVIN_ORG_ID",
97
97
  "SLACK_BOT_TOKEN",
98
98
  "SLACK_APP_TOKEN",
99
+ "SLACK_SIGNING_SECRET",
100
+ "SLACK_MODE",
99
101
  "SLACK_DISABLE",
100
102
  "GITHUB_WEBHOOK_SECRET",
101
103
  "GITHUB_APP_ID",
@@ -409,20 +411,38 @@ describe("setup milestones", () => {
409
411
  });
410
412
  });
411
413
 
412
- test("slack: needs both bot+app tokens AND not disabled", async () => {
414
+ test("slack: reports mode-aware credentials as configured, not live-verified", async () => {
413
415
  process.env.SLACK_BOT_TOKEN = "xoxb-test";
414
416
  const a = await buildStatusPayload();
415
417
  expect(getMilestone(a, "slack").state).toBe("unverified");
416
418
 
417
419
  process.env.SLACK_APP_TOKEN = "xapp-test";
418
420
  const b = await buildStatusPayload();
419
- expect(getMilestone(b, "slack").state).toBe("verified");
421
+ expect(getMilestone(b, "slack").state).toBe("configured");
422
+ expect(getMilestone(b, "slack").hint).toContain("not been verified");
423
+
424
+ process.env.SLACK_MODE = "http";
425
+ delete process.env.SLACK_APP_TOKEN;
426
+ process.env.SLACK_SIGNING_SECRET = "synthetic-signing-secret";
427
+ const http = await buildStatusPayload();
428
+ expect(getMilestone(http, "slack").state).toBe("unverified");
429
+ expect(getMilestone(http, "slack").hint).toContain("unavailable");
420
430
 
421
431
  process.env.SLACK_DISABLE = "true";
422
432
  const c = await buildStatusPayload();
423
433
  expect(getMilestone(c, "slack").state).toBe("unverified");
424
434
  });
425
435
 
436
+ test("slack: invalid mode fails closed with a specific hint", async () => {
437
+ process.env.SLACK_MODE = "webhook";
438
+ process.env.SLACK_BOT_TOKEN = "xoxb-test";
439
+ process.env.SLACK_APP_TOKEN = "xapp-test";
440
+
441
+ const milestone = getMilestone(await buildStatusPayload(), "slack");
442
+ expect(milestone.state).toBe("unverified");
443
+ expect(milestone.hint).toContain("Invalid SLACK_MODE");
444
+ });
445
+
426
446
  test("github: needs webhook secret + app id + private key", async () => {
427
447
  process.env.GITHUB_WEBHOOK_SECRET = "secret";
428
448
  process.env.GITHUB_APP_ID = "12345";
@@ -144,6 +144,41 @@ describe("task steering core", () => {
144
144
  return started!;
145
145
  }
146
146
 
147
+ test("sender labels resolve on task reads, worker polls, and lifecycle responses", async () => {
148
+ const task = await runningTask("pi", "sender labels");
149
+ const user = await createUser({ name: "Taras" });
150
+ const unnamed = await createUser({ name: " " });
151
+ for (const [creator, expected] of [
152
+ [{ createdByKind: "user", createdByUserId: user.id }, "Taras (user)"],
153
+ [
154
+ { createdByKind: "agent", createdByAgentId: agentIds.get("lead")! },
155
+ "Steering lead (agent)",
156
+ ],
157
+ [{ createdByKind: "system" }, "system"],
158
+ [{ createdByKind: "user", createdByUserId: unnamed.id }, `${unnamed.id} (user)`],
159
+ [{ createdByKind: "agent", createdByAgentId: "deleted-agent" }, "deleted-agent (agent)"],
160
+ [{ createdByKind: "user" }, "Unknown (user)"],
161
+ ] as const) {
162
+ const message = await createSteeringMessage({
163
+ taskId: task.id,
164
+ body: "sender test",
165
+ mode: "queue",
166
+ source: "api",
167
+ ...creator,
168
+ });
169
+ expect(message.senderLabel).toBe(expected);
170
+ expect((await getSteeringMessageById(message.id))?.senderLabel).toBe(expected);
171
+ const taskRead = await api("GET", `/api/tasks/${task.id}/steering-messages`);
172
+ expect(taskRead.status).toBe(200);
173
+ expect(taskRead.body.messages).toContainEqual(
174
+ expect.objectContaining({ id: message.id, senderLabel: expected }),
175
+ );
176
+ expect(await getPendingSteeringForAgent(agentIds.get("pi")!)).toContainEqual(message);
177
+ expect((await markSteeringDelivered(message.id, "queue"))?.senderLabel).toBe(expected);
178
+ expect((await markSteeringHandled(message.id))?.senderLabel).toBe(expected);
179
+ }
180
+ });
181
+
147
182
  test("row lifecycle transitions pending -> delivered -> handled", async () => {
148
183
  const task = await runningTask("pi", "lifecycle");
149
184
  const created = await createSteeringMessage({
@@ -0,0 +1,39 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { configureDbResolver, resetDbResolver } from "../prompts/resolver";
3
+ import { renderSteeringDelivery } from "../prompts/steering-delivery";
4
+
5
+ afterEach(resetDbResolver);
6
+
7
+ describe("steering delivery sender", () => {
8
+ test.each([
9
+ "Taras (user)",
10
+ "Lead (agent)",
11
+ "system",
12
+ "deleted-user (user)",
13
+ ])("renders %s and preserves the acknowledgement marker", async (sender) => {
14
+ const id = crypto.randomUUID();
15
+ expect(await renderSteeringDelivery(id, "change course", sender)).toContain(
16
+ `[steering ${id}] From ${sender}: change course`,
17
+ );
18
+ });
19
+
20
+ test("sender is a template variable and whitespace is flattened", async () => {
21
+ configureDbResolver(() => ({
22
+ template: { id: "custom", scope: "global", body: "{{sender}}: {{body}}" },
23
+ }));
24
+ expect(await renderSteeringDelivery("id", "body", "Taras\n(user)")).toBe("Taras (user): body");
25
+ });
26
+
27
+ test("falls back to the bare body on skipped, blank, and failing templates", async () => {
28
+ for (const resolver of [
29
+ () => ({ skip: true as const }),
30
+ () => ({ template: { id: "blank", scope: "global", body: " " } }),
31
+ () => {
32
+ throw new Error("unavailable");
33
+ },
34
+ ]) {
35
+ configureDbResolver(resolver);
36
+ expect(await renderSteeringDelivery("id", "body", "Taras (user)")).toBe("body");
37
+ }
38
+ });
39
+ });
@@ -190,7 +190,7 @@ describe("steering worker transport", () => {
190
190
  });
191
191
 
192
192
  test("delivers pending rows once and reports the adapter's actual mode", async () => {
193
- const pending = pendingMessage();
193
+ const pending = pendingMessage({ createdByKind: "agent", senderLabel: "Lead (agent)" });
194
194
  const nonPending = pendingMessage({ id: crypto.randomUUID(), status: "delivered" });
195
195
  const deliveries: Array<{ mode: string; text: string }> = [];
196
196
  const reports: Array<{ path: string; body: unknown }> = [];
@@ -220,7 +220,7 @@ describe("steering worker transport", () => {
220
220
  // The body is wrapped in the delivery envelope, which MUST carry the
221
221
  // steering message ID — `accept-steer` needs it, and it is the only route
222
222
  // to `handled`. Without it the agent obeys but can never acknowledge.
223
- expect(deliveries[0]?.text).toContain("change course");
223
+ expect(deliveries[0]?.text).toContain("From Lead (agent): change course");
224
224
  expect(deliveries[0]?.text).toContain(pending.id);
225
225
  expect(deliveries[0]?.text).toContain("accept-steer");
226
226
  expect(state.dispatchedIds.has(pending.id)).toBe(true);
@@ -73,6 +73,8 @@ async function pins(client: Client): Promise<string[]> {
73
73
  .sort();
74
74
  }
75
75
 
76
+ // Eight real MCP sessions approach the 10s CI default under parallel runner load.
77
+ // Keep the session-lifetime assertions together with room for connection setup.
76
78
  test("wire response is default-on, per-task, additive, and fixed for one MCP session", async () => {
77
79
  const task = await createTaskExtended("review fixture", { agentId, taskType: "review" });
78
80
  const other = await createTaskExtended("other fixture", { agentId, taskType: "other" });
@@ -115,7 +117,7 @@ test("wire response is default-on, per-task, additive, and fixed for one MCP ses
115
117
  await configure("TASK_TOOL_PRELOAD_ENABLED", "true");
116
118
  await configure("TASK_TOOL_MANIFESTS", "invalid deployment JSON");
117
119
  expect(await pins(await connect(task.id))).toEqual([]);
118
- });
120
+ }, 30_000);
119
121
 
120
122
  test("a manifest cannot expand the scripts-only capability surface", async () => {
121
123
  const task = await createTaskExtended("script fixture", { agentId, taskType: "script" });
@@ -794,6 +794,24 @@ describe("initTelemetry", () => {
794
794
  ).toBe(false);
795
795
  });
796
796
 
797
+ test("Slack HTTP mode uses the signing secret instead of an app token", () => {
798
+ expect(
799
+ _hasSlackChannel({
800
+ SLACK_MODE: "http",
801
+ SLACK_BOT_TOKEN: "xoxb-1",
802
+ SLACK_SIGNING_SECRET: "synthetic-signing-secret",
803
+ }),
804
+ ).toBe(true);
805
+ expect(_hasSlackChannel({ SLACK_MODE: "http", SLACK_BOT_TOKEN: "xoxb-1" })).toBe(false);
806
+ expect(
807
+ _hasSlackChannel({
808
+ SLACK_MODE: "invalid",
809
+ SLACK_BOT_TOKEN: "xoxb-1",
810
+ SLACK_APP_TOKEN: "xapp-1",
811
+ }),
812
+ ).toBe(false);
813
+ });
814
+
797
815
  test("Email requires AGENTMAIL_WEBHOOK_SECRET", () => {
798
816
  expect(_hasEmailChannel({})).toBe(false);
799
817
  expect(_hasEmailChannel({ AGENTMAIL_WEBHOOK_SECRET: "whsec_1" })).toBe(true);
@@ -0,0 +1,126 @@
1
+ import { afterAll, beforeAll, expect, spyOn, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import {
4
+ closeDb,
5
+ createWorkflow,
6
+ createWorkflowRun,
7
+ deleteWorkflow,
8
+ getWorkflowRun,
9
+ getWorkflowRunStepsByRunId,
10
+ initDb,
11
+ } from "../be/db";
12
+ import { codeLevelTriage } from "../heartbeat/heartbeat";
13
+ import { getExecutorRegistry } from "../workflows";
14
+ import { isWorkflowRunActive, walkGraph } from "../workflows/engine";
15
+ import { ScriptExecutor } from "../workflows/executors/script";
16
+ import { recoverIncompleteRuns } from "../workflows/recovery";
17
+
18
+ const dbPath = `./test-workflow-live-recovery-${crypto.randomUUID()}.sqlite`;
19
+ const workflowIds: string[] = [];
20
+ const output = { exitCode: 0, stdout: "done", stderr: "" };
21
+
22
+ beforeAll(() => initDb(dbPath));
23
+ afterAll(async () => {
24
+ for (const id of workflowIds) await deleteWorkflow(id);
25
+ closeDb();
26
+ for (const suffix of ["", "-wal", "-shm"]) await unlink(dbPath + suffix).catch(() => {});
27
+ });
28
+
29
+ test("periodic heartbeat leaves an in-flight long script owned and activates its successor once", async () => {
30
+ const workflow = await createWorkflow({
31
+ name: "live recovery regression",
32
+ definition: {
33
+ nodes: [
34
+ {
35
+ id: "long-script",
36
+ type: "script",
37
+ config: { runtime: "bash", script: "sleep 90", timeout: 120_000 },
38
+ next: "successor",
39
+ },
40
+ { id: "successor", type: "script", config: { runtime: "bash", script: "echo done" } },
41
+ ],
42
+ },
43
+ });
44
+ workflowIds.push(workflow.id);
45
+ const runId = crypto.randomUUID();
46
+ await createWorkflowRun({ id: runId, workflowId: workflow.id, triggerType: "manual" });
47
+ const entered = Promise.withResolvers<void>();
48
+ const release = Promise.withResolvers<void>();
49
+ const calls: string[] = [];
50
+ // Hold the executor at an explicit barrier instead of sleeping for 90 seconds.
51
+ // A duplicate invocation returns immediately so the broken recovery fails
52
+ // assertions rather than hanging the test.
53
+ const executor = spyOn(ScriptExecutor.prototype, "run").mockImplementation(async (input) => {
54
+ calls.push(input.meta.nodeId);
55
+ if (calls.length === 1) {
56
+ entered.resolve();
57
+ await release.promise;
58
+ }
59
+ return { status: "success", output };
60
+ });
61
+ const walking = walkGraph(
62
+ workflow.definition,
63
+ runId,
64
+ {},
65
+ [workflow.definition.nodes[0]!],
66
+ getExecutorRegistry(),
67
+ workflow.id,
68
+ );
69
+ try {
70
+ await entered.promise;
71
+ for (let sweep = 0; sweep < 2; sweep++) {
72
+ const findings = await codeLevelTriage();
73
+ expect(findings.staleCleanup.workflowRuns).toBe(0);
74
+ const run = await getWorkflowRun(runId);
75
+ expect(run?.status).toBe("running");
76
+ expect(run?.finishedAt).toBeFalsy();
77
+ expect(calls).toEqual(["long-script"]);
78
+ }
79
+ release.resolve();
80
+ await walking;
81
+ expect(calls).toEqual(["long-script", "successor"]);
82
+ expect((await getWorkflowRun(runId))?.status).toBe("completed");
83
+ expect((await getWorkflowRunStepsByRunId(runId)).map((step) => step.status)).toEqual([
84
+ "completed",
85
+ "completed",
86
+ ]);
87
+ expect(isWorkflowRunActive(runId)).toBe(false);
88
+ } finally {
89
+ release.resolve();
90
+ await walking;
91
+ executor.mockRestore();
92
+ }
93
+ });
94
+
95
+ test("an interrupted run without a live walk still recovers and executes its successor", async () => {
96
+ const workflow = await createWorkflow({
97
+ name: "interrupted recovery control",
98
+ definition: {
99
+ nodes: [
100
+ {
101
+ id: "first",
102
+ type: "script",
103
+ config: { runtime: "bash", script: "echo first" },
104
+ next: "last",
105
+ },
106
+ { id: "last", type: "script", config: { runtime: "bash", script: "echo last" } },
107
+ ],
108
+ },
109
+ });
110
+ workflowIds.push(workflow.id);
111
+ const runId = crypto.randomUUID();
112
+ await createWorkflowRun({ id: runId, workflowId: workflow.id, triggerType: "manual" });
113
+ const calls: string[] = [];
114
+ const executor = spyOn(ScriptExecutor.prototype, "run").mockImplementation(async (input) => {
115
+ calls.push(input.meta.nodeId);
116
+ return { status: "success", output };
117
+ });
118
+ try {
119
+ expect(await recoverIncompleteRuns(getExecutorRegistry())).toBe(1);
120
+ expect(calls).toEqual(["first", "last"]);
121
+ expect((await getWorkflowRun(runId))?.status).toBe("completed");
122
+ expect(isWorkflowRunActive(runId)).toBe(false);
123
+ } finally {
124
+ executor.mockRestore();
125
+ }
126
+ });
@@ -11,7 +11,7 @@ describe("resolveWorkflowLlmConfig", () => {
11
11
  expect(config).toEqual({
12
12
  apiKey: "sk-or-test",
13
13
  baseURL: "https://gateway.example.test/v1",
14
- model: "google/gemini-3-flash-preview",
14
+ model: "deepseek/deepseek-v4.1-flash",
15
15
  });
16
16
  });
17
17
 
@@ -23,7 +23,7 @@ describe("resolveWorkflowLlmConfig", () => {
23
23
  expect(config).toEqual({
24
24
  apiKey: "example-sk-openai-test",
25
25
  baseURL: undefined,
26
- model: "gpt-5.4-mini",
26
+ model: "gpt-6-luna",
27
27
  });
28
28
  });
29
29
 
@@ -34,7 +34,7 @@ describe("resolveWorkflowLlmConfig", () => {
34
34
  const config = await resolveWorkflowLlmConfig(undefined, {
35
35
  OPENAI_API_KEY: "example-sk-openai-test",
36
36
  });
37
- expect(config.model).toBe("gpt-5.4-mini");
37
+ expect(config.model).toBe("gpt-6-luna");
38
38
  } finally {
39
39
  if (previous === undefined) delete process.env.MEMORY_RATER_MODEL;
40
40
  else process.env.MEMORY_RATER_MODEL = previous;