@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
package/src/server.ts CHANGED
@@ -7,6 +7,7 @@ import { isSteeringEnabled } from "./be/steering";
7
7
  import { registerGithubTaskReactions } from "./github/task-reactions";
8
8
  import { loadGlobalConfigsIntoEnv } from "./http/core";
9
9
  import { isChatSdkEnabled } from "./integrations/chat-sdk/config";
10
+ import { MCP_MAX_TOOL_INPUT_ELEMENTS } from "./mcp-limits";
10
11
  import { resolveTemplate } from "./prompts/resolver";
11
12
  import { isRbacEnabled } from "./rbac";
12
13
  import { getSlackConfiguration } from "./slack/config";
@@ -450,6 +451,7 @@ export async function createServer(
450
451
  capabilities: {
451
452
  logging: {},
452
453
  },
454
+ maxToolInputElements: MCP_MAX_TOOL_INPUT_ELEMENTS,
453
455
  },
454
456
  );
455
457
 
@@ -204,13 +204,13 @@ export function createAssistant(): Assistant {
204
204
  // Fetch shared files before the task text is final: a file that can't be
205
205
  // downloaded is flagged in it.
206
206
  if (files?.length) await safeSetStatus("Downloading attachments...");
207
- await using inbound = await fetchSlackFiles(client, files);
207
+ await using inbound = await fetchSlackFiles(client, files, { transcribe: true });
208
208
  // Any in-body `<@U…>` mention the requester typed is rewritten via
209
209
  // the identity primitive before it reaches agent-visible task text —
210
210
  // never a raw Slack ID. Bot-mention routing checks above use the
211
211
  // raw `messageText`, not this rendered copy.
212
212
  const renderedMessageText = await rewriteSlackMentions(
213
- buildEffectiveText(messageText, inbound.files, inbound.failed),
213
+ buildEffectiveText(messageText, inbound.files, inbound.failed, inbound.notes),
214
214
  cachedBotUserId ?? undefined,
215
215
  );
216
216
 
@@ -22,6 +22,8 @@ export interface SlackFile {
22
22
  title?: string;
23
23
  mimetype: string;
24
24
  filetype: string;
25
+ /** `slack_audio` on a voice clip recorded in Slack. */
26
+ subtype?: string;
25
27
  size: number;
26
28
  url_private: string;
27
29
  url_private_download: string;
@@ -702,8 +702,8 @@ export function registerMessageHandler(app: App): void {
702
702
  return ignoreSlackInbound("rate_limited");
703
703
  }
704
704
 
705
- await using inbound = await fetchSlackFiles(client, msg.files);
706
- const taskText = buildEffectiveText(msg.text, inbound.files, inbound.failed);
705
+ await using inbound = await fetchSlackFiles(client, msg.files, { transcribe: true });
706
+ const taskText = buildEffectiveText(msg.text, inbound.files, inbound.failed, inbound.notes);
707
707
  const taskDescription = isImplicitMention
708
708
  ? taskText
709
709
  : extractTaskFromMessage(taskText, botUserId);
@@ -778,9 +778,9 @@ export function registerMessageHandler(app: App): void {
778
778
  }
779
779
 
780
780
  // Extract task description (the text plus a `[File: …]` line per attachment)
781
- await using inbound = await fetchSlackFiles(client, msg.files);
781
+ await using inbound = await fetchSlackFiles(client, msg.files, { transcribe: true });
782
782
  const taskDescription = extractTaskFromMessage(
783
- buildEffectiveText(msg.text, inbound.files, inbound.failed),
783
+ buildEffectiveText(msg.text, inbound.files, inbound.failed, inbound.notes),
784
784
  botUserId,
785
785
  );
786
786
  if (!taskDescription) {
@@ -13,6 +13,8 @@
13
13
  * could not be attached — never dropped silently. A download failure is
14
14
  * also flagged on that line; a storage failure happens after the text is
15
15
  * written, so it shows up only in the reply and in the missing attachment.
16
+ * Audio files (voice clips) a user shares get their transcript on the next
17
+ * line when transcription is configured, and keep the audio attached.
16
18
  */
17
19
  import { mkdtemp, rm } from "node:fs/promises";
18
20
  import { tmpdir } from "node:os";
@@ -27,6 +29,7 @@ import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
27
29
  import type { AgentTask, CreateTaskOptions, TaskAttachment } from "../types";
28
30
  import { scrubSecrets } from "../utils/secret-scrubber";
29
31
  import { taskAttachmentFetchCommand } from "../utils/task-attachment-links";
32
+ import { transcribeAudioNotes } from "../utils/transcription";
30
33
  import { getFileInfo, type SlackFile } from "./files";
31
34
  import { noteSlackInboundSideEffect } from "./inbound-dispatch";
32
35
  // Side-effect import: registers all Slack event templates in the in-memory registry
@@ -73,6 +76,8 @@ export type InboundSlackFiles = {
73
76
  files: SlackFile[];
74
77
  fetched: FetchedSlackFile[];
75
78
  failed: SlackFileFailure[];
79
+ /** Task-text note per audio file id (a transcript), filled when fetched with `transcribe`. */
80
+ notes?: Map<string, string>;
76
81
  /** Deletes the downloaded copies; hold the batch with `await using`. */
77
82
  [Symbol.asyncDispose](): Promise<void>;
78
83
  };
@@ -96,9 +101,14 @@ export function formatFileSize(bytes: number): string {
96
101
  * Build a text representation of file attachments for inclusion in messages.
97
102
  * Each file is formatted as: [File: filename.ext (mimetype, size) id=FILE_ID],
98
103
  * followed by `(not attached: <reason>)` when the file never made it into the
99
- * task's attachments.
104
+ * task's attachments, and by the file's note (a voice-clip transcript) on the
105
+ * next line.
100
106
  */
101
- export function buildAttachmentText(files: SlackFile[], failed: SlackFileFailure[] = []): string {
107
+ export function buildAttachmentText(
108
+ files: SlackFile[],
109
+ failed: SlackFileFailure[] = [],
110
+ notes: Map<string, string> = new Map(),
111
+ ): string {
102
112
  const reasons = new Map(failed.map((f) => [f.file.id, f.reason]));
103
113
  return files
104
114
  .map((f) => {
@@ -106,7 +116,8 @@ export function buildAttachmentText(files: SlackFile[], failed: SlackFileFailure
106
116
  const size = typeof f.size === "number" ? formatFileSize(f.size) : "unknown size";
107
117
  const line = `[File: ${f.name ?? f.id} (${f.mimetype ?? "unknown type"}, ${size}) id=${f.id}]`;
108
118
  const reason = reasons.get(f.id);
109
- return reason ? `${line} (not attached: ${reason})` : line;
119
+ const note = notes.get(f.id);
120
+ return [reason ? `${line} (not attached: ${reason})` : line, note].filter(Boolean).join("\n");
110
121
  })
111
122
  .join("\n");
112
123
  }
@@ -121,15 +132,16 @@ export function buildEffectiveText(
121
132
  text: string | undefined,
122
133
  files?: SlackFile[],
123
134
  failed?: SlackFileFailure[],
135
+ notes?: Map<string, string>,
124
136
  ): string {
125
137
  const hasText = !!text?.trim();
126
138
  const hasFiles = files && files.length > 0;
127
139
 
128
140
  if (hasText && hasFiles) {
129
- return `${text}\n\n${buildAttachmentText(files, failed)}`;
141
+ return `${text}\n\n${buildAttachmentText(files, failed, notes)}`;
130
142
  }
131
143
  if (hasFiles) {
132
- return buildAttachmentText(files, failed);
144
+ return buildAttachmentText(files, failed, notes);
133
145
  }
134
146
  return text || "";
135
147
  }
@@ -147,11 +159,14 @@ export function bufferedFileFailures(files: SlackFile[] | undefined): SlackFileF
147
159
  * Download every file on a Slack message with the bot token, each to its own
148
160
  * temp file. Never throws: a file that can't be fetched lands in `failed` with
149
161
  * a reason the user and the agent can read. Hold the result with `await using`
150
- * so the temp files go away with it.
162
+ * so the temp files go away with it. With `transcribe`, every downloaded audio
163
+ * file also gets a `notes` entry: its transcript, the failure, or a line saying
164
+ * no provider is configured.
151
165
  */
152
166
  export async function fetchSlackFiles(
153
167
  client: WebClient,
154
168
  files: SlackFile[] | undefined,
169
+ options: { transcribe?: boolean } = {},
155
170
  ): Promise<InboundSlackFiles> {
156
171
  let dir: string | undefined;
157
172
  const result: InboundSlackFiles = {
@@ -182,9 +197,27 @@ export async function fetchSlackFiles(
182
197
  result.fetched.push({ file, ...outcome });
183
198
  }
184
199
  }
200
+ if (options.transcribe) {
201
+ result.notes = await transcribeAudioNotes(
202
+ result.fetched
203
+ .filter(({ file }) => isSlackAudio(file))
204
+ .map(({ file, path }) => ({
205
+ key: file.id,
206
+ name: file.name ?? file.id,
207
+ data: Bun.file(path),
208
+ mimeType: file.mimetype,
209
+ })),
210
+ "[Slack]",
211
+ );
212
+ }
185
213
  return result;
186
214
  }
187
215
 
216
+ /** An audio file, or a voice clip recorded in Slack (its MIME type can read `video/…`). */
217
+ function isSlackAudio(file: SlackFile): boolean {
218
+ return file.subtype === "slack_audio" || Boolean(file.mimetype?.startsWith("audio/"));
219
+ }
220
+
188
221
  /** Does the start of an HTML response read like Slack's sign-in page? */
189
222
  function looksLikeSignIn(head: Uint8Array[]): boolean {
190
223
  const joined = new Uint8Array(head.reduce((total, part) => total + part.byteLength, 0));
@@ -177,7 +177,7 @@ async function slackFlush(
177
177
  const app = getSlackApp();
178
178
  const files = items.flatMap((item) => item.files ?? []);
179
179
  await using inbound = app
180
- ? await fetchSlackFiles(app.client, files)
180
+ ? await fetchSlackFiles(app.client, files, { transcribe: true })
181
181
  : {
182
182
  files,
183
183
  fetched: [],
@@ -196,6 +196,7 @@ async function slackFlush(
196
196
  item.text,
197
197
  item.files?.map((file) => resolvedFiles.get(file.id) ?? file),
198
198
  inbound.failed,
199
+ inbound.notes,
199
200
  ),
200
201
  )
201
202
  .join("\n---\n"),
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import { getSessionLogsByTaskId } from "@/be/db";
19
- import { getEmbeddingProvider, getMemoryStore } from "@/be/memory";
19
+ import { indexMemoryContent, taskCompletionMemoryKey } from "@/be/memory/index-content";
20
20
  import { getRetrievalsForTask } from "@/be/memory/raters/retrieval";
21
21
  import { runServerRaters } from "@/be/memory/raters/run-server-raters";
22
22
  import { shouldPersistTaskCompletionMemory } from "@/memory/automatic-task-gate";
@@ -55,21 +55,22 @@ export async function runTaskTerminalEffects(args: {
55
55
  // Skip indexing if there's truly no content
56
56
  if (taskContent.length < 30) return;
57
57
 
58
- const store = getMemoryStore();
59
- const provider = getEmbeddingProvider();
60
-
61
- const memory = await store.store({
58
+ // Same path as memory-store: chunk, store, and embed each chunk, so a
59
+ // long output never meets the embedding model's input cap. The per-task
60
+ // key makes the chunks one document, like a keyed manual memory, and a
61
+ // repeat write for the task replaces that document atomically.
62
+ const key = taskCompletionMemoryKey(taskId);
63
+ await indexMemoryContent({
62
64
  agentId: agentId ?? null,
63
65
  content: taskContent,
64
66
  name: `Task: ${safeTask.slice(0, 80)}`,
65
67
  scope: "agent",
66
68
  source: "task_completion",
67
69
  sourceTaskId: taskId,
70
+ key,
71
+ intent: "task completion memory",
72
+ replaceExisting: true,
68
73
  });
69
- const embedding = await provider.embed(taskContent);
70
- if (embedding) {
71
- await store.updateEmbedding(memory.id, embedding, provider.name);
72
- }
73
74
 
74
75
  // Auto-promote high-value completions to swarm memory (P3)
75
76
  const shouldShareWithSwarm =
@@ -80,24 +81,27 @@ export async function runTaskTerminalEffects(args: {
80
81
 
81
82
  if (shouldShareWithSwarm) {
82
83
  try {
83
- const swarmMemory = await store.store({
84
+ await indexMemoryContent({
84
85
  agentId: agentId ?? null,
85
- scope: "swarm",
86
- name: `Shared: ${safeTask.slice(0, 80)}`,
87
86
  content: `Task completed by agent ${agentId}:\n\n${taskContent}`,
87
+ name: `Shared: ${safeTask.slice(0, 80)}`,
88
+ scope: "swarm",
88
89
  source: "task_completion",
89
90
  sourceTaskId: taskId,
91
+ key,
92
+ intent: "task completion memory",
93
+ replaceExisting: true,
90
94
  });
91
- const swarmEmbedding = await provider.embed(taskContent);
92
- if (swarmEmbedding) {
93
- await store.updateEmbedding(swarmMemory.id, swarmEmbedding, provider.name);
94
- }
95
95
  } catch {
96
96
  // Non-blocking — swarm memory promotion failure is not critical
97
97
  }
98
98
  }
99
- } catch {
99
+ } catch (err) {
100
100
  // Non-blocking — task completion memory failure should not affect task status
101
+ console.error(
102
+ "[task-terminal-effects] task completion memory write failed:",
103
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
104
+ );
101
105
  }
102
106
  })().catch((err) =>
103
107
  console.error(
@@ -0,0 +1,197 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { parseAppDefinition } from "../apps/definition";
3
+
4
+ /**
5
+ * Validator coverage for `CardGrid` and the Table column `format` template.
6
+ * Pure `parseAppDefinition` calls — no DB, no HTTP — mirroring
7
+ * `apps-catalog-additions.test.ts`.
8
+ */
9
+
10
+ type Element = { type: string; props?: Record<string, unknown>; children?: string[] };
11
+
12
+ function appWith(elements: Record<string, Element>) {
13
+ return {
14
+ models: {
15
+ doc: {
16
+ columns: {
17
+ title: { kind: "string", required: true },
18
+ author: { kind: "string" },
19
+ cover: { kind: "string" },
20
+ status: { kind: "enum", enum: ["draft", "published"], default: "draft" },
21
+ },
22
+ },
23
+ },
24
+ queries: {
25
+ docs: { model: "doc" },
26
+ docById: { model: "doc", filter: { id: { $param: "doc" } } },
27
+ },
28
+ pages: {
29
+ main: { title: "Main", root: "root", elements },
30
+ detail: {
31
+ title: "Detail",
32
+ params: { doc: { kind: "string" }, position: { kind: "number" } },
33
+ root: "detailRoot",
34
+ elements: { detailRoot: { type: "Text", props: { content: "Detail" } } },
35
+ },
36
+ },
37
+ defaultPage: "main",
38
+ };
39
+ }
40
+
41
+ async function issuesFor(input: unknown) {
42
+ const parsed = await parseAppDefinition(input);
43
+ return parsed.success ? [] : parsed.issues;
44
+ }
45
+
46
+ const el = (id: string) => `pages.main.elements.${id}`;
47
+
48
+ function cardGrid(props: Record<string, unknown>) {
49
+ return appWith({
50
+ root: { type: "Stack", props: {}, children: ["cards"] },
51
+ cards: {
52
+ type: "CardGrid",
53
+ props: {
54
+ data: { $state: "/queries/docs/data" },
55
+ loading: { $state: "/queries/docs/loading" },
56
+ error: { $state: "/queries/docs/error" },
57
+ titleKey: "title",
58
+ ...props,
59
+ },
60
+ },
61
+ });
62
+ }
63
+
64
+ const navigateToDetail = [
65
+ { action: "app.navigate", params: { page: "detail", params: { doc: { $row: "id" } } } },
66
+ ];
67
+
68
+ describe("CardGrid", () => {
69
+ test("onClick running app.navigate to a declared page with $row params passes", async () => {
70
+ const issues = await issuesFor(
71
+ cardGrid({
72
+ imageKey: "cover",
73
+ fields: [
74
+ { key: "author" },
75
+ { key: "createdAt", kind: "date" },
76
+ { key: "status", kind: "badge", tones: { published: "success" } },
77
+ ],
78
+ columns: { base: 1, md: 2, lg: 4 },
79
+ emptyMessage: "No documents yet",
80
+ onClick: navigateToDetail,
81
+ }),
82
+ );
83
+ if (issues.length > 0) console.error(issues);
84
+ expect(issues).toEqual([]);
85
+ });
86
+
87
+ test("$row and $rowIndex are accepted inside the onClick chain", async () => {
88
+ const issues = await issuesFor(
89
+ cardGrid({
90
+ onClick: [
91
+ { action: "app.refresh", params: { query: "docs" } },
92
+ {
93
+ action: "app.navigate",
94
+ params: {
95
+ page: "detail",
96
+ params: { doc: { $row: "id" }, position: { $rowIndex: true } },
97
+ },
98
+ },
99
+ ],
100
+ }),
101
+ );
102
+ if (issues.length > 0) console.error(issues);
103
+ expect(issues).toEqual([]);
104
+ });
105
+
106
+ test("$row outside the onClick chain is rejected", async () => {
107
+ const issues = await issuesFor(cardGrid({ emptyMessage: { $row: "title" } }));
108
+ expect(issues).toContainEqual({
109
+ path: `${el("cards")}.props.emptyMessage`,
110
+ message: "$row, $rowIndex, $form, and $chain bindings are only allowed in action params",
111
+ });
112
+ });
113
+
114
+ test("onClick naming an unknown action fails", async () => {
115
+ const issues = await issuesFor(cardGrid({ onClick: [{ action: "app.nope", params: {} }] }));
116
+ expect(issues).toContainEqual({
117
+ path: `${el("cards")}.props.onClick.0.action`,
118
+ message: 'unknown action type "app.nope"',
119
+ });
120
+ });
121
+
122
+ test("onClick navigating to an undeclared page fails", async () => {
123
+ const issues = await issuesFor(
124
+ cardGrid({ onClick: [{ action: "app.navigate", params: { page: "nope" } }] }),
125
+ );
126
+ expect(issues).toContainEqual({
127
+ path: `${el("cards")}.props.onClick.0.params.page`,
128
+ message: 'unknown page "nope"',
129
+ });
130
+ });
131
+
132
+ test("titleKey is required", async () => {
133
+ const copy = cardGrid({});
134
+ delete copy.pages.main.elements.cards!.props!.titleKey;
135
+ const issues = await issuesFor(copy);
136
+ expect(issues.some((i) => i.path.startsWith(`${el("cards")}.props`))).toBe(true);
137
+ });
138
+
139
+ test("data bound to a $state path passes", async () => {
140
+ const issues = await issuesFor(
141
+ appWith({
142
+ root: { type: "Stack", props: {}, children: ["cards"] },
143
+ cards: {
144
+ type: "CardGrid",
145
+ props: { data: { $state: "/queries/docs/data" }, titleKey: "title" },
146
+ },
147
+ }),
148
+ );
149
+ if (issues.length > 0) console.error(issues);
150
+ expect(issues).toEqual([]);
151
+ });
152
+
153
+ test("an unknown field kind is rejected", async () => {
154
+ const issues = await issuesFor(cardGrid({ fields: [{ key: "author", kind: "status" }] }));
155
+ expect(issues.some((i) => i.path.startsWith(`${el("cards")}.props.fields.0.kind`))).toBe(true);
156
+ });
157
+ });
158
+
159
+ describe("Table column format", () => {
160
+ test("a column with a {{field}} format template passes", async () => {
161
+ const issues = await issuesFor(
162
+ appWith({
163
+ root: { type: "Stack", props: {}, children: ["table"] },
164
+ table: {
165
+ type: "Table",
166
+ props: {
167
+ data: { $state: "/queries/docs/data" },
168
+ columns: [
169
+ { key: "title", label: "Document", format: "{{title}} ({{status}})" },
170
+ { key: "author" },
171
+ ],
172
+ },
173
+ },
174
+ }),
175
+ );
176
+ if (issues.length > 0) console.error(issues);
177
+ expect(issues).toEqual([]);
178
+ });
179
+
180
+ test("format must be a string", async () => {
181
+ const issues = await issuesFor(
182
+ appWith({
183
+ root: { type: "Stack", props: {}, children: ["table"] },
184
+ table: {
185
+ type: "Table",
186
+ props: {
187
+ data: { $state: "/queries/docs/data" },
188
+ columns: [{ key: "title", format: 42 }],
189
+ },
190
+ },
191
+ }),
192
+ );
193
+ expect(issues.some((i) => i.path.startsWith(`${el("table")}.props.columns.0.format`))).toBe(
194
+ true,
195
+ );
196
+ });
197
+ });
@@ -0,0 +1,120 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { parseAppDefinition } from "../apps/definition";
3
+
4
+ /**
5
+ * Validator coverage for the small catalog additions: `NumberInput`, Select
6
+ * `defaultValue`, and `fill` on Stack / Split. Pure `parseAppDefinition`
7
+ * calls — no DB, no HTTP — mirroring `apps-catalog-inputs.test.ts`.
8
+ */
9
+
10
+ type Element = { type: string; props?: Record<string, unknown>; children?: string[] };
11
+
12
+ function appWith(elements: Record<string, Element>) {
13
+ return {
14
+ models: {},
15
+ pages: { main: { title: "Main", root: "root", elements } },
16
+ defaultPage: "main",
17
+ };
18
+ }
19
+
20
+ async function issuesFor(input: unknown) {
21
+ const parsed = await parseAppDefinition(input);
22
+ return parsed.success ? [] : parsed.issues;
23
+ }
24
+
25
+ const el = (id: string) => `pages.main.elements.${id}`;
26
+
27
+ describe("NumberInput", () => {
28
+ test("a NumberInput's /ui/<id>/value read by a bound Text validates", async () => {
29
+ const issues = await issuesFor(
30
+ appWith({
31
+ root: { type: "Stack", props: {}, children: ["width", "echo"] },
32
+ width: {
33
+ type: "NumberInput",
34
+ props: { id: "x", label: "Width", placeholder: "px", min: 1, max: 4096 },
35
+ },
36
+ echo: { type: "Text", props: { content: { $state: "/ui/x/value" } } },
37
+ }),
38
+ );
39
+ if (issues.length > 0) console.error(issues);
40
+ expect(issues).toEqual([]);
41
+ });
42
+
43
+ test("a /ui/<id>/value read with no UI control of that id is rejected", async () => {
44
+ const issues = await issuesFor(
45
+ appWith({
46
+ root: { type: "Stack", props: {}, children: ["echo"] },
47
+ echo: { type: "Text", props: { content: { $state: "/ui/x/value" } } },
48
+ }),
49
+ );
50
+ expect(issues).toContainEqual({
51
+ path: `${el("echo")}.props.content`,
52
+ message: 'state reference targets unknown UI control "x"',
53
+ });
54
+ });
55
+
56
+ test("id is required and min/max must be numbers", async () => {
57
+ const missingId = await issuesFor(
58
+ appWith({
59
+ root: { type: "Stack", props: {}, children: ["width"] },
60
+ width: { type: "NumberInput", props: { label: "Width" } },
61
+ }),
62
+ );
63
+ expect(missingId.map((issue) => issue.path)).toContain(`${el("width")}.props.id`);
64
+
65
+ const badBounds = await issuesFor(
66
+ appWith({
67
+ root: { type: "Stack", props: {}, children: ["width"] },
68
+ width: { type: "NumberInput", props: { id: "x", min: "1", max: "10" } },
69
+ }),
70
+ );
71
+ const paths = badBounds.map((issue) => issue.path);
72
+ expect(paths).toContain(`${el("width")}.props.min`);
73
+ expect(paths).toContain(`${el("width")}.props.max`);
74
+ });
75
+ });
76
+
77
+ describe("Select defaultValue", () => {
78
+ const select = (props: Record<string, unknown>) =>
79
+ appWith({
80
+ root: { type: "Stack", props: {}, children: ["device", "echo"] },
81
+ device: {
82
+ type: "Select",
83
+ props: { id: "device", options: ["phone", "tablet", "desktop"], ...props },
84
+ },
85
+ echo: { type: "Text", props: { content: { $state: "/ui/device/value" } } },
86
+ });
87
+
88
+ test("a Select with defaultValue validates", async () => {
89
+ const issues = await issuesFor(select({ defaultValue: "tablet" }));
90
+ if (issues.length > 0) console.error(issues);
91
+ expect(issues).toEqual([]);
92
+ });
93
+
94
+ test("defaultValue must be a string", async () => {
95
+ const paths = (await issuesFor(select({ defaultValue: 2 }))).map((issue) => issue.path);
96
+ expect(paths).toContain(`${el("device")}.props.defaultValue`);
97
+ });
98
+ });
99
+
100
+ describe("Stack and Split fill", () => {
101
+ const layout = (stackFill: unknown, splitFill: unknown) =>
102
+ appWith({
103
+ root: { type: "Stack", props: { fill: stackFill }, children: ["split"] },
104
+ split: { type: "Split", props: { fill: splitFill, ratio: "1-1" }, children: ["a", "b"] },
105
+ a: { type: "Text", props: { content: "left" } },
106
+ b: { type: "Text", props: { content: "right" } },
107
+ });
108
+
109
+ test("fill: true validates on both", async () => {
110
+ const issues = await issuesFor(layout(true, true));
111
+ if (issues.length > 0) console.error(issues);
112
+ expect(issues).toEqual([]);
113
+ });
114
+
115
+ test("fill must be a boolean", async () => {
116
+ const paths = (await issuesFor(layout("yes", 1))).map((issue) => issue.path);
117
+ expect(paths).toContain(`${el("root")}.props.fill`);
118
+ expect(paths).toContain(`${el("split")}.props.fill`);
119
+ });
120
+ });