@desplega.ai/agent-swarm 2.4.3 → 2.4.5

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 (270) hide show
  1. package/dist/{acp-adapter-1885dpr7.js → acp-adapter-2wdkktv6.js} +1 -2
  2. package/dist/{actions-pn76n32q.js → actions-c40ebcm4.js} +18 -18
  3. package/dist/{amp-adapter-af84rx1d.js → amp-adapter-wb67m95z.js} +2 -2
  4. package/dist/{app-6sv8mhzm.js → app-353vre8g.js} +3 -3
  5. package/dist/{assistant-sc2tf825.js → assistant-sj1njh23.js} +21 -21
  6. package/dist/{boot-reembed-y3m0ydzj.js → boot-reembed-b9c3az3c.js} +1 -1
  7. package/dist/{boot-reembed-gvxjs2py.js → boot-reembed-qdkvxeem.js} +1 -1
  8. package/dist/{boot-scrub-sweep-1j9mk05f.js → boot-scrub-sweep-m3wh8eks.js} +2 -2
  9. package/dist/{boot-seeds-37sp626b.js → boot-seeds-j2273s54.js} +4 -4
  10. package/dist/{claude-adapter-rp0jtnwr.js → claude-adapter-m7q0a085.js} +2 -2
  11. package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-5ptb6tzs.js} +2 -2
  12. package/dist/{claude-managed-setup-rdt248k7.js → claude-managed-setup-smrqk5j5.js} +3 -3
  13. package/dist/{claude-sdk-session-53rcw5we.js → claude-sdk-session-38xnz21e.js} +8284 -8267
  14. package/dist/{cli-6eyncg0y.js → cli-018r4jq6.js} +34 -6
  15. package/dist/{cli-sds5zzs8.js → cli-1f3qzw6y.js} +78 -12
  16. package/dist/{cli-f0p0f6ja.js → cli-1sp861s4.js} +7 -7
  17. package/dist/{cli-dm8p2dpf.js → cli-1syn2xy1.js} +10 -10
  18. package/dist/{cli-tkf4x6ha.js → cli-37j04k05.js} +11 -11
  19. package/dist/{cli-v87dr6e3.js → cli-3a45zcgm.js} +1 -1
  20. package/dist/{cli-1n9gdg56.js → cli-43x64hv3.js} +4 -4
  21. package/dist/{cli-p0qerd2r.js → cli-4zfqgjc5.js} +2 -2
  22. package/dist/{cli-aa3tb29r.js → cli-7c9qs530.js} +1 -1
  23. package/dist/{cli-9qtjfd60.js → cli-848hxdwj.js} +1 -1
  24. package/dist/{cli-1xsraagq.js → cli-94gwyg7m.js} +8 -8
  25. package/dist/{cli-0dxxwfkt.js → cli-9hstj09h.js} +17 -14
  26. package/dist/{cli-1012xb7j.js → cli-9yqgm9nd.js} +22 -22
  27. package/dist/{cli-pygdsgh8.js → cli-a92r1127.js} +1 -1
  28. package/dist/{cli-8vverzf7.js → cli-ba8ne1es.js} +5 -0
  29. package/dist/{cli-bxrqvpg5.js → cli-be2161h1.js} +7 -7
  30. package/dist/{cli-npfe9t1s.js → cli-bnes4z6b.js} +2 -2
  31. package/dist/{cli-aypbg9r2.js → cli-cxxe61f6.js} +1 -1
  32. package/dist/{cli-vq9gqarq.js → cli-dk44dxec.js} +1 -1
  33. package/dist/{cli-836ehxmt.js → cli-eww120g8.js} +3 -2
  34. package/dist/{cli-a0kcty51.js → cli-f01h25yt.js} +1 -1
  35. package/dist/{cli-mzkwkbdy.js → cli-f0g3pgvf.js} +1 -1
  36. package/dist/{cli-1ma335fz.js → cli-ftc1g5tj.js} +2 -2
  37. package/dist/{cli-4yef8ghv.js → cli-fv5hepyy.js} +3 -3
  38. package/dist/{cli-58vwn100.js → cli-g35m2xwm.js} +4 -4
  39. package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
  40. package/dist/{cli-gx0j2khs.js → cli-j99azg3g.js} +3 -3
  41. package/dist/{cli-er83cjbn.js → cli-k8g16gz3.js} +416 -116
  42. package/dist/{cli-qp2m4btv.js → cli-kpw04va0.js} +2 -2
  43. package/dist/{cli-8y99kfey.js → cli-ms85309d.js} +33 -15
  44. package/dist/{cli-kgnkyabk.js → cli-n72rg53p.js} +1555 -645
  45. package/dist/{cli-e4d78xsv.js → cli-p14m2cnp.js} +1 -1
  46. package/dist/{cli-1eszr343.js → cli-qfxc84gh.js} +1 -1
  47. package/dist/{cli-wk28hzqn.js → cli-qwhy2qma.js} +1 -1
  48. package/dist/{cli-73ezfhha.js → cli-st8aw1m6.js} +228 -11
  49. package/dist/{cli-zvbb4n0e.js → cli-tw4p90jb.js} +92 -192
  50. package/dist/{cli-038fga5k.js → cli-v0yrfmdh.js} +148 -183
  51. package/dist/{cli-6aen3ezb.js → cli-v8fjakwf.js} +1 -1
  52. package/dist/{cli-99jez72r.js → cli-w9etnnxk.js} +6 -1
  53. package/dist/{cli-vcgscaan.js → cli-wt0jf9hm.js} +217 -80
  54. package/dist/{cli-s9rr4gr2.js → cli-xpa64rek.js} +1065 -305
  55. package/dist/{cli-h0mgxann.js → cli-z7d29rw9.js} +2 -2
  56. package/dist/cli.js +45 -20
  57. package/dist/{codex-adapter-2zrmab9b.js → codex-adapter-tn6k5mkg.js} +1 -1
  58. package/dist/{codex-session-runner-kbkz62vc.js → codex-session-runner-7vjt7jbw.js} +1 -1
  59. package/dist/{commands-hd574th6.js → commands-t7rz9146.js} +2 -2
  60. package/dist/{controller-pz5mcfg4.js → controller-m2jasma5.js} +2 -2
  61. package/dist/{controllers-connect-5rgq2hfk.js → controllers-connect-9ftpex99.js} +19 -19
  62. package/dist/{cursor-adapter-mgaprq0j.js → cursor-adapter-mjv79e4z.js} +1 -1
  63. package/dist/{db-6nf1207q.js → db-6sfrn9g3.js} +1 -1
  64. package/dist/{db-h06dxp05.js → db-rn673r06.js} +3 -3
  65. package/dist/{dispatcher-zthpy4m1.js → dispatcher-c155xz0p.js} +17 -17
  66. package/dist/{dsh-adapter-64dhw0j7.js → dsh-adapter-3mmszc1x.js} +1 -1
  67. package/dist/{external-channel-x01t7ke6.js → external-channel-35k6tkv8.js} +2 -2
  68. package/dist/{grok-adapter-h21jv594.js → grok-adapter-tfrq7tg1.js} +2 -3
  69. package/dist/handlers-7gw64dxw.js +39 -0
  70. package/dist/{heartbeat-zzfksckz.js → heartbeat-dyg4npkp.js} +20 -20
  71. package/dist/{hook-z4zhzj1f.js → hook-drspbwpe.js} +1 -1
  72. package/dist/{hook-3xfqhh2s.js → hook-x26b31bg.js} +1 -1
  73. package/dist/{http-q813tmya.js → http-pk972xyw.js} +571 -294
  74. package/dist/{index-va3y63es.js → index-6srwvd5b.js} +1 -2
  75. package/dist/{index-x8xbyn1t.js → index-arytgj5q.js} +1 -1
  76. package/dist/{index-wbbs14c5.js → index-bgdyvcnv.js} +17 -17
  77. package/dist/{index-s98037v0.js → index-cbxkmfc5.js} +1 -2
  78. package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
  79. package/dist/{index-m0a4ydqj.js → index-gabbcdn2.js} +17 -17
  80. package/dist/{index-220tk9bw.js → index-jwxkkex2.js} +139 -76
  81. package/dist/index-tfb26zhy.js +41 -0
  82. package/dist/{keepalive-y5t1my7m.js → keepalive-7yrng7px.js} +3 -3
  83. package/dist/{lead-2xwtq66b.js → lead-7e02z839.js} +29 -30
  84. package/dist/{maintenance-ybfsaedv.js → maintenance-e6cemdqt.js} +1 -1
  85. package/dist/{node-3v3f08f7.js → node-de6akczr.js} +10 -9
  86. package/dist/{oauth-refresh-sweep-909jmn12.js → oauth-refresh-sweep-yw3xdbcj.js} +2 -2
  87. package/dist/{onboard-xn6fkkys.js → onboard-0a9y5yrc.js} +2 -2
  88. package/dist/{otel-impl-e6mn0z13.js → otel-impl-trjpkfb7.js} +1 -1
  89. package/dist/{pricing-refresh-ygd362ep.js → pricing-refresh-d2b5my0y.js} +3 -3
  90. package/dist/{rbac-roles-mb470p4e.js → rbac-roles-hey3yv9d.js} +2 -2
  91. package/dist/{rbac-roles-c9axvfnz.js → rbac-roles-nxh5ahwk.js} +2 -2
  92. package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
  93. package/dist/{render-v2-g0306nh1.js → render-v2-rr3p2atg.js} +3 -3
  94. package/dist/{resume-v315hnq6.js → resume-2efkf1s1.js} +5 -5
  95. package/dist/{session-access-2zg285a1.js → session-access-s2mttxpw.js} +6 -4
  96. package/dist/{setup-a4mtf02k.js → setup-vypma5yt.js} +1 -1
  97. package/dist/{work-objects-9fr0fgtn.js → work-objects-rvhckdkp.js} +20 -20
  98. package/dist/{worker-qae22jeb.js → worker-5snytpbd.js} +29 -30
  99. package/dist/worker-token-xf11yyds.js +136 -0
  100. package/openapi.json +303 -13
  101. package/package.json +8 -8
  102. package/src/apps/action-runner.ts +4 -0
  103. package/src/apps/catalog.generated.json +209 -4
  104. package/src/apps/page-validator.ts +21 -3
  105. package/src/be/chunking.ts +42 -9
  106. package/src/be/db/tasks/draft-text.ts +15 -0
  107. package/src/be/db/tasks/read.ts +14 -1
  108. package/src/be/db.ts +5 -1
  109. package/src/be/memory/index-content.ts +36 -17
  110. package/src/be/memory/providers/openai-embedding.ts +115 -61
  111. package/src/be/memory/providers/sqlite-store.ts +69 -23
  112. package/src/be/memory/reranker.ts +31 -1
  113. package/src/be/memory/search-candidates.ts +47 -0
  114. package/src/be/memory/types.ts +14 -0
  115. package/src/be/migrations/232_agent_session_tokens.sql +41 -0
  116. package/src/be/scripts/typecheck.ts +2 -0
  117. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +116 -55
  118. package/src/be/seed-scripts/index.ts +2 -2
  119. package/src/be/swarm-config-guard.ts +40 -1
  120. package/src/be/task-attachment-store.ts +30 -0
  121. package/src/be/users.ts +169 -12
  122. package/src/cli.tsx +24 -0
  123. package/src/commands/worker-token.ts +164 -0
  124. package/src/extensions/catalog.generated.json +4 -4
  125. package/src/extensions/contract-types.generated.ts +1 -1
  126. package/src/extensions/contract.ts +7 -0
  127. package/src/fs/provider.ts +13 -1
  128. package/src/heartbeat/heartbeat.ts +31 -11
  129. package/src/http/apps.ts +43 -5
  130. package/src/http/codex-oauth-keep-warm.ts +27 -9
  131. package/src/http/config.ts +5 -3
  132. package/src/http/core.ts +2 -0
  133. package/src/http/fs.ts +54 -34
  134. package/src/http/mcp-body.ts +37 -0
  135. package/src/http/mcp-user.ts +3 -5
  136. package/src/http/mcp.ts +6 -7
  137. package/src/http/memory.ts +26 -15
  138. package/src/http/page-proxy.ts +61 -5
  139. package/src/http/pages-public.ts +27 -44
  140. package/src/http/pages.ts +4 -4
  141. package/src/http/script-runs.ts +2 -0
  142. package/src/http/session-access.ts +51 -2
  143. package/src/http/sessions.ts +249 -10
  144. package/src/http/tasks.ts +4 -32
  145. package/src/http/users.ts +12 -1
  146. package/src/integrations/chat-sdk/adapters.ts +7 -3
  147. package/src/integrations/chat-sdk/attachments.ts +90 -12
  148. package/src/integrations/chat-sdk/facts.ts +120 -0
  149. package/src/integrations/chat-sdk/inbound.ts +156 -8
  150. package/src/integrations/chat-sdk/limits.ts +24 -0
  151. package/src/integrations/chat-sdk/outbound-files.ts +186 -0
  152. package/src/integrations/chat-sdk/outbound.ts +234 -4
  153. package/src/integrations/chat-sdk/relay.ts +4 -1
  154. package/src/integrations/chat-sdk/router.ts +28 -2
  155. package/src/integrations/chat-sdk/steering.ts +9 -0
  156. package/src/integrations/chat-sdk/templates.ts +14 -0
  157. package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
  158. package/src/mcp-limits.ts +20 -0
  159. package/src/pages/browser-sdk.ts +8 -3
  160. package/src/pages/feedback-overlay.ts +1 -1
  161. package/src/providers/acp-adapter.ts +33 -13
  162. package/src/providers/acp-swarm-events.ts +1 -1
  163. package/src/providers/amp-adapter.ts +1 -1
  164. package/src/providers/codex-adapter.ts +106 -119
  165. package/src/providers/codex-agents-md.ts +35 -229
  166. package/src/providers/codex-oauth/storage.ts +1 -1
  167. package/src/providers/cursor-adapter.ts +1 -1
  168. package/src/rbac/legacy-policy.ts +3 -1
  169. package/src/rbac/permissions.ts +4 -0
  170. package/src/realtime/transport.ts +1 -2
  171. package/src/script-workflows/workflow-ctx.ts +1 -0
  172. package/src/scripts-runtime/types/stdlib.d.ts +2 -0
  173. package/src/scripts-runtime/types/swarm-extension.d.ts +8 -0
  174. package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
  175. package/src/server-user.ts +2 -0
  176. package/src/server.ts +2 -0
  177. package/src/slack/assistant.ts +2 -2
  178. package/src/slack/files.ts +2 -0
  179. package/src/slack/handlers.ts +4 -4
  180. package/src/slack/inbound-files.ts +39 -6
  181. package/src/slack/thread-buffer.ts +2 -1
  182. package/src/tasks/task-terminal-effects.ts +21 -17
  183. package/src/tests/acp-adapter.test.ts +10 -23
  184. package/src/tests/acp-fake-agent.ts +36 -0
  185. package/src/tests/acp-swarm-events.test.ts +1 -1
  186. package/src/tests/agent-session-tokens.test.ts +452 -0
  187. package/src/tests/apps-action-parent-task.test.ts +295 -0
  188. package/src/tests/apps-card-grid.test.ts +197 -0
  189. package/src/tests/apps-catalog-additions.test.ts +120 -0
  190. package/src/tests/apps-tree.test.ts +129 -0
  191. package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
  192. package/src/tests/chat-sdk-config.test.ts +11 -0
  193. package/src/tests/chat-sdk-facts.test.ts +23 -0
  194. package/src/tests/chat-sdk-inbound.test.ts +558 -4
  195. package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
  196. package/src/tests/codex-adapter.test.ts +167 -242
  197. package/src/tests/codex-oauth-keep-warm.test.ts +85 -1
  198. package/src/tests/config-api-only-keys.test.ts +9 -0
  199. package/src/tests/config-session-auth.test.ts +14 -0
  200. package/src/tests/cors-allowlist.test.ts +1 -1
  201. package/src/tests/docs-index-footprint.test.js +2 -2
  202. package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
  203. package/src/tests/grok-adapter.test.ts +3 -3
  204. package/src/tests/heartbeat-checklist.test.ts +33 -0
  205. package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
  206. package/src/tests/heartbeat-jev-gate.test.ts +265 -1
  207. package/src/tests/kv-page-proxy.test.ts +91 -5
  208. package/src/tests/launch-password-rejection.test.ts +2 -2
  209. package/src/tests/mcp-input-caps.test.ts +294 -0
  210. package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
  211. package/src/tests/memory-embedding-input-cap.test.ts +114 -0
  212. package/src/tests/memory.test.ts +14 -0
  213. package/src/tests/migration-232-agent-session-tokens.test.ts +67 -0
  214. package/src/tests/page-guest-session.test.ts +19 -8
  215. package/src/tests/page-proxy-authed.test.ts +1 -1
  216. package/src/tests/page-proxy.test.ts +17 -10
  217. package/src/tests/page-session.test.ts +102 -1
  218. package/src/tests/pages-authed-mode.test.ts +82 -12
  219. package/src/tests/pages-password-mode.test.ts +5 -5
  220. package/src/tests/rbac-engine.test.ts +1 -0
  221. package/src/tests/realtime-transport.test.ts +13 -7
  222. package/src/tests/runtime-child-token-cleanup.test.ts +3 -3
  223. package/src/tests/script-connections.test.ts +2 -0
  224. package/src/tests/script-runs-http.test.ts +84 -0
  225. package/src/tests/scripts-http.test.ts +10 -1
  226. package/src/tests/session-route-posture.test.ts +2 -0
  227. package/src/tests/swarm-config-export-to-env.test.ts +22 -3
  228. package/src/tests/task-audio-transcription.test.ts +299 -0
  229. package/src/tests/task-completion-memory-chunking.test.ts +434 -0
  230. package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
  231. package/src/tests/transcription.test.ts +299 -0
  232. package/src/tests/worker-token-cli.test.ts +76 -0
  233. package/src/tests/workflow-executors.test.ts +4 -2
  234. package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
  235. package/src/tests/workflow-system-one-decision.test.ts +2 -1
  236. package/src/tests/workflow-system-one-extraction.test.ts +805 -0
  237. package/src/tools/chat-access.ts +10 -0
  238. package/src/tools/chat-post.ts +31 -11
  239. package/src/tools/chat-reply.ts +32 -10
  240. package/src/tools/memory-search.ts +3 -1
  241. package/src/tools/system-one-decide.ts +17 -4
  242. package/src/types.ts +8 -1
  243. package/src/utils/config-env-export.ts +3 -1
  244. package/src/utils/page-session.ts +133 -20
  245. package/src/utils/request-auth-context.ts +2 -1
  246. package/src/utils/transcription.ts +246 -0
  247. package/src/workflows/definition.ts +7 -1
  248. package/src/workflows/engine.ts +18 -12
  249. package/src/workflows/executors/index.ts +1 -0
  250. package/src/workflows/executors/registry.ts +2 -0
  251. package/src/workflows/executors/system-one-decision.ts +84 -262
  252. package/src/workflows/executors/system-one-extract-wire.ts +57 -0
  253. package/src/workflows/executors/system-one-extraction.ts +830 -0
  254. package/src/workflows/executors/system-one-providers.ts +316 -114
  255. package/src/workflows/executors/system-one-transport.ts +264 -0
  256. package/templates/extensions/slack-reply-gate/README.md +24 -22
  257. package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
  258. package/templates/extensions/slack-reply-gate/manifest.json +2 -2
  259. package/templates/official/lead/HEARTBEAT.md +1 -1
  260. package/templates/skills/apps/SKILL.md +31 -7
  261. package/templates/skills/apps/content.md +31 -7
  262. package/templates/skills/chat-sdk/content.md +6 -2
  263. package/templates/skills/code-quality/content.md +2 -1
  264. package/templates/skills/heartbeat-runbook/content.md +8 -8
  265. package/templates/skills/pages/content.md +6 -4
  266. package/templates/skills/swarm-scripts/SKILL.md +2 -2
  267. package/templates/skills/swarm-scripts/content.md +2 -2
  268. package/dist/cli-v1sjhn52.js +0 -7
  269. package/dist/handlers-2qjw434w.js +0 -39
  270. package/dist/index-d0g18bax.js +0 -41
@@ -11,6 +11,7 @@ import {
11
11
  getDbClient,
12
12
  getLatestScriptRunStepTaskByContextKey,
13
13
  getLatestTaskByContextKey,
14
+ getPendingTaskForAgent,
14
15
  initDb,
15
16
  updateScriptRun,
16
17
  } from "../be/db";
@@ -20,6 +21,7 @@ import { handleScriptRuns } from "../http/script-runs";
20
21
  import { getPathSegments, parseQueryParams } from "../http/utils";
21
22
  import { localProcessScriptExecutor } from "../script-workflows/executor";
22
23
  import { setScriptRunExecutor } from "../script-workflows/supervisor";
24
+ import type { AgentTask } from "../types";
23
25
  import {
24
26
  clearVolatileSecretsForTesting,
25
27
  refreshSecretScrubberCache,
@@ -537,6 +539,88 @@ describe("/api/script-runs HTTP", () => {
537
539
  });
538
540
  });
539
541
 
542
+ async function dispatchStepFor(
543
+ stepKey: string,
544
+ stepAgentId: string,
545
+ extra: Record<string, unknown> = {},
546
+ ): Promise<{ response: Promise<TestResponse>; task: AgentTask }> {
547
+ const created = await dispatch("/api/script-runs", {
548
+ method: "POST",
549
+ agentId,
550
+ body: createBody(),
551
+ });
552
+ const { id: runId } = (await created.json()) as { id: string };
553
+ const response = dispatch(`/api/internal/script-runs/${runId}/agent-task`, {
554
+ method: "POST",
555
+ agentId,
556
+ body: JSON.stringify({ stepKey, task: `do ${stepKey}`, agentId: stepAgentId, ...extra }),
557
+ });
558
+ const contextKey = `script-run:${runId}:${stepKey}`;
559
+ let task = await getLatestScriptRunStepTaskByContextKey(contextKey);
560
+ for (let i = 0; i < 50 && !task; i++) {
561
+ await Bun.sleep(20);
562
+ task = await getLatestScriptRunStepTaskByContextKey(contextKey);
563
+ }
564
+ expect(task).not.toBeNull();
565
+ return { response, task: task! };
566
+ }
567
+
568
+ test("agent-task dispatch persists dependsOn and holds the task until the dependency completes", async () => {
569
+ const stepAgent = await createAgent({
570
+ name: "deps-step-worker",
571
+ isLead: false,
572
+ status: "idle",
573
+ });
574
+ const dependency = await createTaskExtended("upstream fix", { source: "mcp" });
575
+
576
+ const { response, task } = await dispatchStepFor("after-fix", stepAgent.id, {
577
+ dependsOn: [dependency.id],
578
+ });
579
+
580
+ expect(task.dependsOn).toEqual([dependency.id]);
581
+ expect(await getPendingTaskForAgent(stepAgent.id)).toBeNull();
582
+
583
+ await completeTask(dependency.id, "fixed");
584
+ expect((await getPendingTaskForAgent(stepAgent.id))?.id).toBe(task.id);
585
+
586
+ await completeTask(task.id, "done");
587
+ expect((await response).status).toBe(200);
588
+ });
589
+
590
+ test("agent-task dispatch without dependsOn is startable at once", async () => {
591
+ const stepAgent = await createAgent({
592
+ name: "no-deps-step-worker",
593
+ isLead: false,
594
+ status: "idle",
595
+ });
596
+
597
+ const { response, task } = await dispatchStepFor("no-deps", stepAgent.id);
598
+
599
+ expect(task.dependsOn).toEqual([]);
600
+ expect((await getPendingTaskForAgent(stepAgent.id))?.id).toBe(task.id);
601
+
602
+ await completeTask(task.id, "done");
603
+ expect((await response).status).toBe(200);
604
+ });
605
+
606
+ test("agent-task dispatch rejects a dependsOn entry that is not a task UUID", async () => {
607
+ const created = await dispatch("/api/script-runs", {
608
+ method: "POST",
609
+ agentId,
610
+ body: createBody(),
611
+ });
612
+ const { id: runId } = (await created.json()) as { id: string };
613
+
614
+ const response = await dispatch(`/api/internal/script-runs/${runId}/agent-task`, {
615
+ method: "POST",
616
+ agentId,
617
+ body: JSON.stringify({ stepKey: "bad-deps", task: "x", dependsOn: ["not-a-uuid"] }),
618
+ });
619
+
620
+ expect(response.status).toBe(400);
621
+ expect(await getLatestScriptRunStepTaskByContextKey(`script-run:${runId}:bad-deps`)).toBeNull();
622
+ });
623
+
540
624
  test("agent-task polling returns the step output when a newer completed follow-up shares its context key", async () => {
541
625
  const created = await dispatch("/api/script-runs", {
542
626
  method: "POST",
@@ -785,6 +785,7 @@ describe("ctx.swarm.config.get through POST /api/scripts/run", () => {
785
785
  missingIsUndefined: ctx.swarm.config.get("SCRIPT_CFG_NOT_SET") === undefined,
786
786
  fallback: read("SCRIPT_CFG_NOT_SET") ?? "fallback",
787
787
  apiOnly: read("SLACK_SIGNING_SECRET"),
788
+ transcriptionKey: read("TRANSCRIPTION_API_KEY"),
788
789
  proto: read("__proto__"),
789
790
  };
790
791
  };`;
@@ -798,6 +799,7 @@ describe("ctx.swarm.config.get through POST /api/scripts/run", () => {
798
799
  missingIsUndefined: boolean;
799
800
  fallback: string;
800
801
  apiOnly: string | null;
802
+ transcriptionKey: string | null;
801
803
  proto: string | null;
802
804
  };
803
805
 
@@ -829,11 +831,17 @@ describe("ctx.swarm.config.get through POST /api/scripts/run", () => {
829
831
  value: `signing-${randomToken(24)}`,
830
832
  isSecret: true,
831
833
  });
834
+ await upsertSwarmConfig({
835
+ scope: "global",
836
+ key: "TRANSCRIPTION_API_KEY",
837
+ value: `stt-${randomToken(24)}`,
838
+ isSecret: true,
839
+ });
832
840
  });
833
841
 
834
842
  afterAll(async () => {
835
843
  await getDbClient().run(
836
- "DELETE FROM swarm_config WHERE key LIKE 'SCRIPT_CFG_%' OR key IN ('SLACK_SIGNING_SECRET', '__proto__')",
844
+ "DELETE FROM swarm_config WHERE key LIKE 'SCRIPT_CFG_%' OR key IN ('SLACK_SIGNING_SECRET', 'TRANSCRIPTION_API_KEY', '__proto__')",
837
845
  );
838
846
  clearVolatileSecretsForTesting();
839
847
  });
@@ -862,6 +870,7 @@ describe("ctx.swarm.config.get through POST /api/scripts/run", () => {
862
870
  missingIsUndefined: true,
863
871
  fallback: "fallback",
864
872
  apiOnly: null,
873
+ transcriptionKey: null,
865
874
  proto: "proto-global",
866
875
  });
867
876
  // The unwrapped secret is scrubbed from everything the run returns.
@@ -25,6 +25,7 @@ import "../http/all-routes";
25
25
  import { handleActiveSessions } from "../http/active-sessions";
26
26
  import { handleApiKeys } from "../http/api-keys";
27
27
  import { handleApprovalRequests } from "../http/approval-requests";
28
+ import { handleApps } from "../http/apps";
28
29
  import { handleContext } from "../http/context";
29
30
  import { handleCore } from "../http/core";
30
31
  import { handleEvents } from "../http/events";
@@ -76,6 +77,7 @@ const HANDLERS: Handler[] = [
76
77
  handleFs,
77
78
  handleActiveSessions,
78
79
  handleSessions,
80
+ handleApps,
79
81
  (req, res, segments, query) => handleApprovalRequests(req, res, segments, query),
80
82
  (req, res, segments, query) => handleApiKeys(req, res, segments, query),
81
83
  (req, res, segments, query) => handleScriptRuns(req, res, segments, query),
@@ -7,7 +7,11 @@ import { closeDb, createAgent, getSwarmConfigs, initDb, upsertSwarmConfig } from
7
7
  import { handleConfig } from "../http/config";
8
8
  import { getPathSegments, parseQueryParams } from "../http/utils";
9
9
  import { registerSetConfigTool } from "../tools/swarm-config/set-config";
10
- import { ENV_EXPORT_RESTRICTED_KEYS, envExportRestrictedError } from "../utils/config-env-export";
10
+ import {
11
+ ENV_EXPORT_RESTRICTED_KEYS,
12
+ effectiveExportToEnv,
13
+ envExportRestrictedError,
14
+ } from "../utils/config-env-export";
11
15
  import { setRequestAuth } from "../utils/request-auth-context";
12
16
  import { clearVolatileSecretsForTesting } from "../utils/secret-scrubber";
13
17
  import { listenOnFreePort } from "./test-net";
@@ -153,6 +157,16 @@ describe("swarm_config exportToEnv", () => {
153
157
  expect(row?.exportToEnv).toBe(false);
154
158
  });
155
159
 
160
+ test("the speech-to-text key never exports, even from an older stored true", async () => {
161
+ const key = "TRANSCRIPTION_API_KEY";
162
+ const stored = await upsertSwarmConfig({ scope: "global", key, value: "t", isSecret: true });
163
+ expect(stored.exportToEnv).toBe(false);
164
+ await expect(
165
+ upsertSwarmConfig({ scope: "global", key, value: "t", isSecret: true, exportToEnv: true }),
166
+ ).rejects.toThrow(envExportRestrictedError(key));
167
+ expect(effectiveExportToEnv(key, true)).toBe(false);
168
+ });
169
+
156
170
  test("the config API and /api/config/resolved carry the flag", async () => {
157
171
  const put = await fetch(`${baseUrl}/api/config`, {
158
172
  method: "PUT",
@@ -231,6 +245,11 @@ describe("migration 215", () => {
231
245
  "X_CLIENT_SECRET",
232
246
  "connection.example.secret",
233
247
  ];
248
+ // Restricted after migration 215 shipped. Row readers force the flag off
249
+ // (effectiveExportToEnv), so an older stored true never reaches an agent.
250
+ const RESTRICTED_AT_215 = [...ENV_EXPORT_RESTRICTED_KEYS].filter(
251
+ (key) => key !== "TRANSCRIPTION_API_KEY",
252
+ );
234
253
 
235
254
  test("backfills true and turns off only the deny list", () => {
236
255
  const db = new Database(":memory:");
@@ -243,7 +262,7 @@ describe("migration 215", () => {
243
262
  const insert = db.prepare(
244
263
  "INSERT INTO swarm_config (id, scope, key, value, isSecret, createdAt, lastUpdatedAt) VALUES (?, 'global', ?, 'v', 1, 'now', 'now')",
245
264
  );
246
- const keys = [...MUST_STAY_EXPORTED, ...DENY_ONLY, ...ENV_EXPORT_RESTRICTED_KEYS];
265
+ const keys = [...MUST_STAY_EXPORTED, ...DENY_ONLY, ...RESTRICTED_AT_215];
247
266
  for (const key of keys) insert.run(crypto.randomUUID(), key);
248
267
 
249
268
  db.exec(readFileSync(MIGRATION, "utf8"));
@@ -257,7 +276,7 @@ describe("migration 215", () => {
257
276
  .map((row) => [row.key, row.exportToEnv]),
258
277
  );
259
278
  for (const key of MUST_STAY_EXPORTED) expect([key, flags[key]]).toEqual([key, 1]);
260
- for (const key of [...DENY_ONLY, ...ENV_EXPORT_RESTRICTED_KEYS]) {
279
+ for (const key of [...DENY_ONLY, ...RESTRICTED_AT_215]) {
261
280
  expect([key, flags[key]]).toEqual([key, 0]);
262
281
  }
263
282
  } finally {
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Audio a user sends from the sessions UI or Slack gets its transcript in the
3
+ * task text, through the same `transcribeAudio` helper as the Chat SDK path.
4
+ * No provider configured, or a failed call: one line says so and the task is
5
+ * still created.
6
+ */
7
+ import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
8
+ import { mkdtemp, rm, unlink } from "node:fs/promises";
9
+ import {
10
+ createServer as createHttpServer,
11
+ type IncomingMessage,
12
+ type Server,
13
+ type ServerResponse,
14
+ } from "node:http";
15
+ import { tmpdir } from "node:os";
16
+ import { join } from "node:path";
17
+ import {
18
+ closeDb,
19
+ createAgent,
20
+ createTaskExtended,
21
+ getTaskAttachments,
22
+ getTaskById,
23
+ initDb,
24
+ } from "../be/db";
25
+ import { resetFileStorageProviderForTests } from "../fs/registry";
26
+ import { handleCore } from "../http/core";
27
+ import { handleFs } from "../http/fs";
28
+ import { getPathSegments, parseQueryParams } from "../http/utils";
29
+ import type { SlackFile } from "../slack/files";
30
+ import {
31
+ buildEffectiveText,
32
+ createSlackTaskWithFiles,
33
+ fetchSlackFiles,
34
+ } from "../slack/inbound-files";
35
+ import { NOT_TRANSCRIBED_NOTE, TRANSCRIPTION_ENV_KEYS } from "../utils/transcription";
36
+ import { listenOnFreePort } from "./test-net";
37
+
38
+ const TEST_DB_PATH = "./test-task-audio-transcription.sqlite";
39
+ const API_KEY = "example-task-audio-key";
40
+ const BOT_TOKEN = "example-task-audio-slack-token";
41
+ const AUDIO = new Uint8Array([0x1a, 0x45, 0xdf, 0xa3, 1, 2, 3, 4, 5]);
42
+ const NO_PROVIDER = NOT_TRANSCRIBED_NOTE;
43
+ const savedEnv = new Map(TRANSCRIPTION_ENV_KEYS.map((key) => [key, process.env[key]]));
44
+
45
+ let api: Server;
46
+ let apiPort: number;
47
+ let slackHost: ReturnType<typeof Bun.serve>;
48
+ let stt: ReturnType<typeof Bun.serve>;
49
+ let sttMode: "ok" | "fail" = "ok";
50
+ let sttFilenames: string[] = [];
51
+ let fsDir: string;
52
+ let agentId: string;
53
+
54
+ async function removeDbFiles(): Promise<void> {
55
+ for (const suffix of ["", "-wal", "-shm"]) await unlink(TEST_DB_PATH + suffix).catch(() => {});
56
+ }
57
+
58
+ function useTranscription(): void {
59
+ process.env.TRANSCRIPTION_API_KEY = "stt-task-audio-key";
60
+ process.env.TRANSCRIPTION_BASE_URL = `http://127.0.0.1:${stt.port}/v1`;
61
+ }
62
+
63
+ beforeAll(async () => {
64
+ await removeDbFiles();
65
+ fsDir = await mkdtemp(join(tmpdir(), "task-audio-"));
66
+ process.env.AGENT_FS_LOCAL_DIR = fsDir;
67
+ delete process.env.AGENT_FS_API_URL;
68
+ resetFileStorageProviderForTests();
69
+ initDb(TEST_DB_PATH);
70
+
71
+ stt = Bun.serve({
72
+ port: 0,
73
+ hostname: "127.0.0.1",
74
+ async fetch(req) {
75
+ const file = (await req.formData()).get("file") as File;
76
+ sttFilenames.push(file.name);
77
+ if (sttMode === "fail") {
78
+ return Response.json({ error: { message: "upstream overloaded" } }, { status: 503 });
79
+ }
80
+ return Response.json({ text: " Ship the release notes today. " });
81
+ },
82
+ });
83
+ slackHost = Bun.serve({
84
+ port: 0,
85
+ fetch: (req) =>
86
+ req.headers.get("authorization") === `Bearer ${BOT_TOKEN}`
87
+ ? new Response(AUDIO, { headers: { "content-type": "audio/webm" } })
88
+ : new Response("forbidden", { status: 403 }),
89
+ });
90
+ api = createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
91
+ const myAgentId = req.headers["x-agent-id"] as string | undefined;
92
+ if (await handleCore(req, res, myAgentId, API_KEY)) return;
93
+ const ok = await handleFs(
94
+ req,
95
+ res,
96
+ getPathSegments(req.url || ""),
97
+ parseQueryParams(req.url || ""),
98
+ myAgentId,
99
+ );
100
+ if (!ok) {
101
+ res.writeHead(404);
102
+ res.end("Not Found");
103
+ }
104
+ });
105
+ apiPort = await listenOnFreePort(api);
106
+ agentId = (await createAgent({ name: "task-audio-lead", isLead: true, status: "idle" })).id;
107
+ });
108
+
109
+ afterAll(async () => {
110
+ await new Promise<void>((resolve) => api.close(() => resolve()));
111
+ stt.stop(true);
112
+ slackHost.stop(true);
113
+ closeDb();
114
+ await removeDbFiles();
115
+ await rm(fsDir, { recursive: true, force: true });
116
+ delete process.env.AGENT_FS_LOCAL_DIR;
117
+ resetFileStorageProviderForTests();
118
+ for (const [key, value] of savedEnv) {
119
+ if (value === undefined) delete process.env[key];
120
+ else process.env[key] = value;
121
+ }
122
+ });
123
+
124
+ beforeEach(() => {
125
+ sttMode = "ok";
126
+ sttFilenames = [];
127
+ // A provider key in the shell (OPENAI_API_KEY, ...) would switch transcription on.
128
+ for (const key of TRANSCRIPTION_ENV_KEYS) delete process.env[key];
129
+ });
130
+
131
+ /** The composer's upload: raw body, `name` query, the file's own content type. */
132
+ function upload(taskId: string, name: string, contentType: string): Promise<Response> {
133
+ return fetch(
134
+ `http://localhost:${apiPort}/api/fs/tasks/${taskId}/files?name=${encodeURIComponent(name)}&intent=user-upload`,
135
+ {
136
+ method: "POST",
137
+ body: AUDIO,
138
+ headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": contentType },
139
+ },
140
+ );
141
+ }
142
+
143
+ async function draftTask(text: string): Promise<string> {
144
+ return (await createTaskExtended(text, { agentId, source: "ui", status: "draft" })).id;
145
+ }
146
+
147
+ describe("sessions UI upload", () => {
148
+ test("a recorded webm clip uploaded to a draft puts its transcript in the task text", async () => {
149
+ useTranscription();
150
+ const taskId = await draftTask("what did I say?");
151
+
152
+ // What the composer's mic button uploads in Chrome and Firefox.
153
+ const name = "recording-2026-10-21-143005.webm";
154
+ const res = await upload(taskId, name, "audio/webm;codecs=opus");
155
+
156
+ expect(res.status).toBe(201);
157
+ expect(sttFilenames).toEqual([name]);
158
+ expect((await getTaskById(taskId))!.task).toBe(
159
+ `what did I say?\n\n[File: ${name} (audio/webm)]\n[Transcript of ${name}]\nShip the release notes today.`,
160
+ );
161
+ expect(await getTaskAttachments(taskId)).toHaveLength(1);
162
+ });
163
+
164
+ test("parallel audio uploads each keep their block", async () => {
165
+ useTranscription();
166
+ const taskId = await draftTask("two notes");
167
+
168
+ const responses = await Promise.all([
169
+ upload(taskId, "a.m4a", "audio/mp4"),
170
+ upload(taskId, "b.m4a", "audio/mp4"),
171
+ ]);
172
+
173
+ expect(responses.map((r) => r.status)).toEqual([201, 201]);
174
+ const text = (await getTaskById(taskId))!.task;
175
+ expect(text).toContain("[Transcript of a.m4a]");
176
+ expect(text).toContain("[Transcript of b.m4a]");
177
+ });
178
+
179
+ test("without a transcription provider the task gets one line saying so", async () => {
180
+ const taskId = await draftTask("listen to this");
181
+
182
+ const res = await upload(taskId, "memo.ogg", "audio/ogg");
183
+
184
+ expect(res.status).toBe(201);
185
+ expect(sttFilenames).toEqual([]);
186
+ expect((await getTaskById(taskId))!.task).toBe(
187
+ `listen to this\n\n[File: memo.ogg (audio/ogg)]\n${NO_PROVIDER}`,
188
+ );
189
+ });
190
+
191
+ test("a failed transcription still stores the upload and says why", async () => {
192
+ useTranscription();
193
+ sttMode = "fail";
194
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
195
+ const taskId = await draftTask("listen to this");
196
+ try {
197
+ const res = await upload(taskId, "memo.ogg", "audio/ogg");
198
+ expect(res.status).toBe(201);
199
+ } finally {
200
+ warn.mockRestore();
201
+ }
202
+
203
+ expect((await getTaskById(taskId))!.task).toContain(
204
+ "[File: memo.ogg (audio/ogg)]\n(transcription failed: upstream overloaded)",
205
+ );
206
+ expect(await getTaskAttachments(taskId)).toHaveLength(1);
207
+ });
208
+
209
+ test("a task out of draft and a non-audio file are left as they are", async () => {
210
+ useTranscription();
211
+ const running = (await createTaskExtended("already dispatched", { agentId, source: "ui" })).id;
212
+ const draft = await draftTask("a picture");
213
+
214
+ expect((await upload(running, "late.ogg", "audio/ogg")).status).toBe(201);
215
+ expect((await upload(draft, "shot.png", "image/png")).status).toBe(201);
216
+
217
+ expect(sttFilenames).toEqual([]);
218
+ expect((await getTaskById(running))!.task).toBe("already dispatched");
219
+ expect((await getTaskById(draft))!.task).toBe("a picture");
220
+ });
221
+ });
222
+
223
+ describe("Slack inbound files", () => {
224
+ function slackFile(id: string, name: string, extra: Partial<SlackFile> = {}): SlackFile {
225
+ return {
226
+ id,
227
+ name,
228
+ mimetype: "audio/webm",
229
+ filetype: "webm",
230
+ size: AUDIO.byteLength,
231
+ url_private: `${slackHost.url}files-pri/T0TEAM-${id}/${name}`,
232
+ url_private_download: `${slackHost.url}files-pri/T0TEAM-${id}/download/${name}`,
233
+ ...extra,
234
+ };
235
+ }
236
+ const client = { token: BOT_TOKEN } as never;
237
+
238
+ /** What the Slack handlers do: fetch, build the task text, create the task. */
239
+ async function slackTask(text: string, files: SlackFile[]) {
240
+ await using inbound = await fetchSlackFiles(client, files, { transcribe: true });
241
+ const description = buildEffectiveText(text, inbound.files, inbound.failed, inbound.notes);
242
+ return (await createSlackTaskWithFiles(description, { agentId, source: "slack" }, inbound))
243
+ .task;
244
+ }
245
+
246
+ test("a voice clip gets its transcript under its file line and stays attached", async () => {
247
+ useTranscription();
248
+ // Slack voice clips are `slack_audio`, whatever their MIME type says.
249
+ const clip = slackFile("F0CLIP0001", "audio_message.mp4", {
250
+ mimetype: "video/mp4",
251
+ subtype: "slack_audio",
252
+ });
253
+
254
+ const task = await slackTask("please do this", [clip]);
255
+
256
+ expect(task.task).toBe(
257
+ `please do this\n\n[File: audio_message.mp4 (video/mp4, 9 B) id=F0CLIP0001]\n[Transcript of audio_message.mp4]\nShip the release notes today.`,
258
+ );
259
+ expect((await getTaskById(task.id))!.status).toBe("pending");
260
+ expect(await getTaskAttachments(task.id)).toHaveLength(1);
261
+ });
262
+
263
+ test("an audio file without a provider gets one line saying so", async () => {
264
+ const task = await slackTask("", [slackFile("F0AUDIO002", "memo.webm")]);
265
+
266
+ expect(task.task).toBe(`[File: memo.webm (audio/webm, 9 B) id=F0AUDIO002]\n${NO_PROVIDER}`);
267
+ expect(sttFilenames).toEqual([]);
268
+ expect(await getTaskAttachments(task.id)).toHaveLength(1);
269
+ });
270
+
271
+ test("a failed transcription never blocks the task", async () => {
272
+ useTranscription();
273
+ sttMode = "fail";
274
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
275
+ let task: Awaited<ReturnType<typeof slackTask>>;
276
+ try {
277
+ task = await slackTask("hi", [slackFile("F0AUDIO003", "memo.webm")]);
278
+ } finally {
279
+ warn.mockRestore();
280
+ }
281
+
282
+ expect(task.task).toContain(
283
+ "[File: memo.webm (audio/webm, 9 B) id=F0AUDIO003]\n(transcription failed: upstream overloaded)",
284
+ );
285
+ expect((await getTaskById(task.id))!.status).toBe("pending");
286
+ });
287
+
288
+ test("an agent's own file fetch and non-audio files are not transcribed", async () => {
289
+ useTranscription();
290
+ await using fetched = await fetchSlackFiles(client, [slackFile("F0AUDIO004", "memo.webm")]);
291
+ expect(fetched.notes).toBeUndefined();
292
+
293
+ const task = await slackTask("see image", [
294
+ slackFile("F0IMAGE005", "shot.png", { mimetype: "image/png", filetype: "png" }),
295
+ ]);
296
+ expect(task.task).not.toContain("Transcript");
297
+ expect(sttFilenames).toEqual([]);
298
+ });
299
+ });