@desplega.ai/agent-swarm 2.4.4 → 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 (221) hide show
  1. package/dist/{acp-adapter-thq04adh.js → acp-adapter-2wdkktv6.js} +1 -1
  2. package/dist/{actions-j6ywh6x5.js → actions-c40ebcm4.js} +18 -18
  3. package/dist/{amp-adapter-k8b5xmj8.js → amp-adapter-wb67m95z.js} +2 -2
  4. package/dist/{app-rb7vc02s.js → app-353vre8g.js} +3 -3
  5. package/dist/{assistant-ftta1zjf.js → assistant-sj1njh23.js} +21 -21
  6. package/dist/{boot-reembed-srp6v6af.js → boot-reembed-b9c3az3c.js} +1 -1
  7. package/dist/{boot-reembed-xns7h5h9.js → boot-reembed-qdkvxeem.js} +1 -1
  8. package/dist/{boot-scrub-sweep-3qqrh2mz.js → boot-scrub-sweep-m3wh8eks.js} +2 -2
  9. package/dist/{boot-seeds-9ks8hce7.js → boot-seeds-j2273s54.js} +4 -4
  10. package/dist/{claude-adapter-k5ksyres.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-n5a5eqks.js → claude-managed-setup-smrqk5j5.js} +3 -3
  13. package/dist/{claude-sdk-session-2j3j4kwn.js → claude-sdk-session-38xnz21e.js} +8284 -8267
  14. package/dist/{cli-cynvc9fj.js → cli-018r4jq6.js} +5 -5
  15. package/dist/{cli-9fhkkpj5.js → cli-1f3qzw6y.js} +1 -1
  16. package/dist/{cli-dqb4n629.js → cli-1sp861s4.js} +7 -7
  17. package/dist/{cli-gqvh8d8a.js → cli-1syn2xy1.js} +10 -10
  18. package/dist/{cli-qkhc5aqy.js → cli-37j04k05.js} +11 -11
  19. package/dist/{cli-1d70crsy.js → cli-3a45zcgm.js} +1 -1
  20. package/dist/{cli-t7cyef5g.js → cli-43x64hv3.js} +4 -4
  21. package/dist/{cli-hj3debgw.js → cli-4zfqgjc5.js} +2 -2
  22. package/dist/{cli-55qmbp6z.js → cli-7c9qs530.js} +1 -1
  23. package/dist/{cli-9qtjfd60.js → cli-848hxdwj.js} +1 -1
  24. package/dist/{cli-pccfmtb5.js → cli-94gwyg7m.js} +7 -7
  25. package/dist/{cli-51y09c6p.js → cli-9hstj09h.js} +5 -5
  26. package/dist/{cli-zrwpctp4.js → cli-9yqgm9nd.js} +22 -22
  27. package/dist/{cli-esxerzg7.js → cli-a92r1127.js} +1 -1
  28. package/dist/{cli-b8k7epj1.js → cli-be2161h1.js} +7 -7
  29. package/dist/{cli-xc27fwzg.js → cli-bnes4z6b.js} +2 -2
  30. package/dist/{cli-zrk6jxv8.js → cli-cxxe61f6.js} +1 -1
  31. package/dist/{cli-amyhm3gj.js → cli-dk44dxec.js} +1 -1
  32. package/dist/{cli-nc35pxb7.js → cli-eww120g8.js} +1 -0
  33. package/dist/{cli-a0kcty51.js → cli-f01h25yt.js} +1 -1
  34. package/dist/{cli-82d3nkc3.js → cli-f0g3pgvf.js} +1 -1
  35. package/dist/{cli-1ephgwnq.js → cli-ftc1g5tj.js} +2 -2
  36. package/dist/{cli-2v045xyf.js → cli-fv5hepyy.js} +3 -3
  37. package/dist/{cli-bnz0r3kc.js → cli-g35m2xwm.js} +4 -4
  38. package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
  39. package/dist/{cli-w6nadkh4.js → cli-j99azg3g.js} +3 -3
  40. package/dist/{cli-er83cjbn.js → cli-k8g16gz3.js} +416 -116
  41. package/dist/{cli-4dg55wsk.js → cli-kpw04va0.js} +1 -1
  42. package/dist/{cli-3cgngyd8.js → cli-ms85309d.js} +33 -15
  43. package/dist/{cli-v3yr2gbg.js → cli-n72rg53p.js} +1471 -635
  44. package/dist/{cli-37yb7307.js → cli-p14m2cnp.js} +1 -1
  45. package/dist/{cli-2zqpkvtt.js → cli-qfxc84gh.js} +1 -1
  46. package/dist/{cli-44159k28.js → cli-qwhy2qma.js} +1 -1
  47. package/dist/{cli-2y91wnbh.js → cli-st8aw1m6.js} +221 -9
  48. package/dist/{cli-vq97g1jq.js → cli-tw4p90jb.js} +92 -192
  49. package/dist/{cli-te6b5wvg.js → cli-v0yrfmdh.js} +1 -1
  50. package/dist/{cli-xj0re5tm.js → cli-v8fjakwf.js} +1 -1
  51. package/dist/{cli-p20r8w42.js → cli-wt0jf9hm.js} +215 -80
  52. package/dist/{cli-q6r7t299.js → cli-xpa64rek.js} +33 -11
  53. package/dist/{cli-vdm20wej.js → cli-z7d29rw9.js} +2 -2
  54. package/dist/cli.js +19 -19
  55. package/dist/{codex-adapter-968xwfy7.js → codex-adapter-tn6k5mkg.js} +1 -1
  56. package/dist/{codex-session-runner-ht4z99fa.js → codex-session-runner-7vjt7jbw.js} +1 -1
  57. package/dist/{commands-pcymaeg9.js → commands-t7rz9146.js} +2 -2
  58. package/dist/{controller-fpjyk46r.js → controller-m2jasma5.js} +2 -2
  59. package/dist/{controllers-connect-mwzx1fh6.js → controllers-connect-9ftpex99.js} +19 -19
  60. package/dist/{cursor-adapter-cp4s2e2f.js → cursor-adapter-mjv79e4z.js} +1 -1
  61. package/dist/{db-xsjg9jqq.js → db-6sfrn9g3.js} +1 -1
  62. package/dist/{db-sgd12bt9.js → db-rn673r06.js} +3 -3
  63. package/dist/{dispatcher-x0jfw2c9.js → dispatcher-c155xz0p.js} +17 -17
  64. package/dist/{dsh-adapter-jrq7ma7h.js → dsh-adapter-3mmszc1x.js} +1 -1
  65. package/dist/{external-channel-8njj4h3b.js → external-channel-35k6tkv8.js} +2 -2
  66. package/dist/{grok-adapter-kkr97v2f.js → grok-adapter-tfrq7tg1.js} +2 -2
  67. package/dist/handlers-7gw64dxw.js +39 -0
  68. package/dist/{heartbeat-wd03zhn0.js → heartbeat-dyg4npkp.js} +20 -20
  69. package/dist/{hook-f0vghj42.js → hook-drspbwpe.js} +1 -1
  70. package/dist/{hook-xzj2ts2p.js → hook-x26b31bg.js} +1 -1
  71. package/dist/{http-4hwxv62p.js → http-pk972xyw.js} +355 -216
  72. package/dist/{index-ww2vznkp.js → index-arytgj5q.js} +1 -1
  73. package/dist/{index-hg0x2cdv.js → index-bgdyvcnv.js} +17 -17
  74. package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
  75. package/dist/{index-gdh8jrdm.js → index-gabbcdn2.js} +17 -17
  76. package/dist/{index-97q4xn03.js → index-jwxkkex2.js} +129 -75
  77. package/dist/index-tfb26zhy.js +41 -0
  78. package/dist/{keepalive-hrh7xnr9.js → keepalive-7yrng7px.js} +3 -3
  79. package/dist/{lead-ee8vazz2.js → lead-7e02z839.js} +29 -29
  80. package/dist/{maintenance-e8wnwnqx.js → maintenance-e6cemdqt.js} +1 -1
  81. package/dist/{node-3v3f08f7.js → node-de6akczr.js} +10 -9
  82. package/dist/{oauth-refresh-sweep-zm0yj4sj.js → oauth-refresh-sweep-yw3xdbcj.js} +2 -2
  83. package/dist/{onboard-dtwcq603.js → onboard-0a9y5yrc.js} +2 -2
  84. package/dist/{otel-impl-w3fbhpn0.js → otel-impl-trjpkfb7.js} +1 -1
  85. package/dist/{pricing-refresh-j38dehfs.js → pricing-refresh-d2b5my0y.js} +3 -3
  86. package/dist/{rbac-roles-ejxx9b35.js → rbac-roles-hey3yv9d.js} +2 -2
  87. package/dist/{rbac-roles-ae8yttq4.js → rbac-roles-nxh5ahwk.js} +1 -1
  88. package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
  89. package/dist/{render-v2-n16n0phe.js → render-v2-rr3p2atg.js} +3 -3
  90. package/dist/{resume-z0fgjc38.js → resume-2efkf1s1.js} +5 -5
  91. package/dist/{session-access-cv3mq6mc.js → session-access-s2mttxpw.js} +4 -4
  92. package/dist/{setup-mafhj3yt.js → setup-vypma5yt.js} +1 -1
  93. package/dist/{work-objects-8b86mgjp.js → work-objects-rvhckdkp.js} +20 -20
  94. package/dist/{worker-16s75hm5.js → worker-5snytpbd.js} +29 -29
  95. package/openapi.json +9 -2
  96. package/package.json +7 -7
  97. package/src/apps/catalog.generated.json +206 -4
  98. package/src/apps/page-validator.ts +13 -1
  99. package/src/be/chunking.ts +42 -9
  100. package/src/be/db/tasks/draft-text.ts +15 -0
  101. package/src/be/db/tasks/read.ts +14 -1
  102. package/src/be/db.ts +5 -1
  103. package/src/be/memory/index-content.ts +36 -17
  104. package/src/be/memory/providers/openai-embedding.ts +115 -61
  105. package/src/be/memory/providers/sqlite-store.ts +69 -23
  106. package/src/be/memory/reranker.ts +31 -1
  107. package/src/be/memory/search-candidates.ts +47 -0
  108. package/src/be/memory/types.ts +14 -0
  109. package/src/be/scripts/typecheck.ts +2 -0
  110. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +106 -54
  111. package/src/be/seed-scripts/index.ts +2 -2
  112. package/src/be/swarm-config-guard.ts +24 -1
  113. package/src/be/task-attachment-store.ts +30 -0
  114. package/src/extensions/catalog.generated.json +4 -4
  115. package/src/extensions/contract-types.generated.ts +1 -1
  116. package/src/extensions/contract.ts +7 -0
  117. package/src/fs/provider.ts +13 -1
  118. package/src/heartbeat/heartbeat.ts +31 -11
  119. package/src/http/fs.ts +54 -34
  120. package/src/http/mcp-body.ts +37 -0
  121. package/src/http/mcp-user.ts +3 -5
  122. package/src/http/mcp.ts +6 -7
  123. package/src/http/memory.ts +26 -15
  124. package/src/http/page-proxy.ts +61 -5
  125. package/src/http/pages-public.ts +27 -44
  126. package/src/http/pages.ts +4 -4
  127. package/src/http/script-runs.ts +2 -0
  128. package/src/http/users.ts +12 -1
  129. package/src/integrations/chat-sdk/adapters.ts +7 -3
  130. package/src/integrations/chat-sdk/attachments.ts +90 -12
  131. package/src/integrations/chat-sdk/facts.ts +120 -0
  132. package/src/integrations/chat-sdk/inbound.ts +156 -8
  133. package/src/integrations/chat-sdk/limits.ts +24 -0
  134. package/src/integrations/chat-sdk/outbound-files.ts +186 -0
  135. package/src/integrations/chat-sdk/outbound.ts +234 -4
  136. package/src/integrations/chat-sdk/relay.ts +4 -1
  137. package/src/integrations/chat-sdk/router.ts +28 -2
  138. package/src/integrations/chat-sdk/steering.ts +9 -0
  139. package/src/integrations/chat-sdk/templates.ts +14 -0
  140. package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
  141. package/src/mcp-limits.ts +20 -0
  142. package/src/pages/browser-sdk.ts +8 -3
  143. package/src/pages/feedback-overlay.ts +1 -1
  144. package/src/providers/amp-adapter.ts +1 -1
  145. package/src/providers/codex-adapter.ts +106 -119
  146. package/src/providers/codex-agents-md.ts +35 -229
  147. package/src/providers/cursor-adapter.ts +1 -1
  148. package/src/realtime/transport.ts +1 -2
  149. package/src/script-workflows/workflow-ctx.ts +1 -0
  150. package/src/scripts-runtime/types/stdlib.d.ts +2 -0
  151. package/src/scripts-runtime/types/swarm-extension.d.ts +8 -0
  152. package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
  153. package/src/server-user.ts +2 -0
  154. package/src/server.ts +2 -0
  155. package/src/slack/assistant.ts +2 -2
  156. package/src/slack/files.ts +2 -0
  157. package/src/slack/handlers.ts +4 -4
  158. package/src/slack/inbound-files.ts +39 -6
  159. package/src/slack/thread-buffer.ts +2 -1
  160. package/src/tasks/task-terminal-effects.ts +21 -17
  161. package/src/tests/apps-card-grid.test.ts +197 -0
  162. package/src/tests/apps-catalog-additions.test.ts +120 -0
  163. package/src/tests/apps-tree.test.ts +129 -0
  164. package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
  165. package/src/tests/chat-sdk-config.test.ts +11 -0
  166. package/src/tests/chat-sdk-facts.test.ts +23 -0
  167. package/src/tests/chat-sdk-inbound.test.ts +558 -4
  168. package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
  169. package/src/tests/codex-adapter.test.ts +167 -242
  170. package/src/tests/config-api-only-keys.test.ts +9 -0
  171. package/src/tests/cors-allowlist.test.ts +1 -1
  172. package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
  173. package/src/tests/heartbeat-checklist.test.ts +33 -0
  174. package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
  175. package/src/tests/heartbeat-jev-gate.test.ts +242 -1
  176. package/src/tests/kv-page-proxy.test.ts +91 -5
  177. package/src/tests/launch-password-rejection.test.ts +2 -2
  178. package/src/tests/mcp-input-caps.test.ts +294 -0
  179. package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
  180. package/src/tests/memory-embedding-input-cap.test.ts +114 -0
  181. package/src/tests/memory.test.ts +14 -0
  182. package/src/tests/page-guest-session.test.ts +19 -8
  183. package/src/tests/page-proxy-authed.test.ts +1 -1
  184. package/src/tests/page-proxy.test.ts +17 -10
  185. package/src/tests/page-session.test.ts +102 -1
  186. package/src/tests/pages-authed-mode.test.ts +82 -12
  187. package/src/tests/pages-password-mode.test.ts +5 -5
  188. package/src/tests/realtime-transport.test.ts +13 -7
  189. package/src/tests/script-runs-http.test.ts +84 -0
  190. package/src/tests/scripts-http.test.ts +10 -1
  191. package/src/tests/swarm-config-export-to-env.test.ts +22 -3
  192. package/src/tests/task-audio-transcription.test.ts +299 -0
  193. package/src/tests/task-completion-memory-chunking.test.ts +434 -0
  194. package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
  195. package/src/tests/transcription.test.ts +299 -0
  196. package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
  197. package/src/tests/workflow-system-one-decision.test.ts +2 -1
  198. package/src/tools/chat-access.ts +10 -0
  199. package/src/tools/chat-post.ts +31 -11
  200. package/src/tools/chat-reply.ts +32 -10
  201. package/src/tools/memory-search.ts +3 -1
  202. package/src/tools/system-one-decide.ts +17 -4
  203. package/src/types.ts +6 -0
  204. package/src/utils/config-env-export.ts +3 -1
  205. package/src/utils/page-session.ts +133 -20
  206. package/src/utils/transcription.ts +246 -0
  207. package/src/workflows/executors/system-one-decision.ts +28 -7
  208. package/src/workflows/executors/system-one-providers.ts +39 -4
  209. package/templates/extensions/slack-reply-gate/README.md +24 -22
  210. package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
  211. package/templates/extensions/slack-reply-gate/manifest.json +2 -2
  212. package/templates/skills/apps/SKILL.md +28 -6
  213. package/templates/skills/apps/content.md +28 -6
  214. package/templates/skills/chat-sdk/content.md +6 -2
  215. package/templates/skills/code-quality/content.md +2 -1
  216. package/templates/skills/heartbeat-runbook/content.md +5 -5
  217. package/templates/skills/pages/content.md +6 -4
  218. package/templates/skills/swarm-scripts/SKILL.md +2 -2
  219. package/templates/skills/swarm-scripts/content.md +2 -2
  220. package/dist/handlers-0pvee9nw.js +0 -39
  221. package/dist/index-thm0y6a8.js +0 -41
@@ -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
+ });