@desplega.ai/agent-swarm 1.145.0 → 1.147.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/README.md +2 -1
  2. package/dist/{acp-adapter-580t41c0.js → acp-adapter-9n319wqc.js} +61 -6
  3. package/dist/{actions-m662tjng.js → actions-2vxqvpr9.js} +9 -8
  4. package/dist/{app-zb3rndm8.js → app-w066xfy0.js} +6 -5
  5. package/dist/assistant-kt31dj2h.js +236 -0
  6. package/dist/{boot-reembed-072x00vn.js → boot-reembed-az5rassp.js} +7 -6
  7. package/dist/{boot-reembed-65zz2smt.js → boot-reembed-j3mm3rfz.js} +8 -7
  8. package/dist/{boot-scrub-logs-yz0ams04.js → boot-scrub-logs-b6h54817.js} +4 -4
  9. package/dist/{claude-adapter-paj270y2.js → claude-adapter-w1z04ann.js} +8 -6
  10. package/dist/{claude-managed-setup-w619jncn.js → claude-managed-setup-f5pmsyrp.js} +24 -3
  11. package/dist/{claude-sdk-session-swt80j9z.js → claude-sdk-session-f0cc1kec.js} +8 -6
  12. package/dist/{cli-dt553ktb.js → cli-0b6y7kxm.js} +1 -1
  13. package/dist/{cli-bxdhw615.js → cli-12fz972k.js} +4 -4
  14. package/dist/{cli-s2f20r5w.js → cli-1th1728g.js} +2 -2
  15. package/dist/{cli-rs6ec3b9.js → cli-3tgwnf43.js} +3 -3
  16. package/dist/{cli-gxnzc6wh.js → cli-4nr988h6.js} +1 -1
  17. package/dist/{cli-axs8e6d4.js → cli-53s590z8.js} +1 -1
  18. package/dist/{cli-ygbjp3z8.js → cli-54wwve05.js} +5 -4
  19. package/dist/{cli-v0asr5yv.js → cli-719k9j2c.js} +2 -2
  20. package/dist/{cli-anktweqr.js → cli-89xfd7f9.js} +21 -47
  21. package/dist/{cli-q34a0e10.js → cli-8v0g1yc8.js} +1 -1
  22. package/dist/cli-9d8ntetj.js +42 -0
  23. package/dist/{cli-z9fprcrf.js → cli-atcve9y8.js} +881 -738
  24. package/dist/{cli-jmb7g47h.js → cli-b5bq9crk.js} +14 -31
  25. package/dist/cli-d8brzxjb.js +190 -0
  26. package/dist/{cli-gzky0gkr.js → cli-de14znh8.js} +2 -2
  27. package/dist/{cli-xytwff7f.js → cli-dttph1bw.js} +2 -2
  28. package/dist/cli-ep19k3mb.js +19 -0
  29. package/dist/{cli-m1xa8be7.js → cli-f146mn5s.js} +7 -50
  30. package/dist/{cli-1fj0chdk.js → cli-f7dsy61y.js} +5 -3
  31. package/dist/{cli-f567d5c1.js → cli-ftht3zzj.js} +2782 -2589
  32. package/dist/{cli-a0g1gbt5.js → cli-hbv0kq6w.js} +2 -2
  33. package/dist/{cli-fhsz3twr.js → cli-jsp14b6c.js} +171 -1
  34. package/dist/{cli-7fnkj1hj.js → cli-kpv03zhj.js} +21 -17
  35. package/dist/{cli-m4j6trn4.js → cli-m8yrsg97.js} +2 -2
  36. package/dist/cli-m9sxbkhm.js +779 -0
  37. package/dist/cli-mmemxxdc.js +823 -0
  38. package/dist/{cli-tqtvj5pt.js → cli-n8508nre.js} +1 -1
  39. package/dist/{cli-z96t0thj.js → cli-p1d5b073.js} +37 -2
  40. package/dist/{cli-gh8y8f36.js → cli-rw51bq3j.js} +13 -10
  41. package/dist/{cli-59ej1c07.js → cli-smby5m17.js} +7 -6
  42. package/dist/{cli-65vqzz3v.js → cli-tzvk9haz.js} +94 -372
  43. package/dist/{cli-dc9k2ba9.js → cli-w9kb3bk1.js} +1 -1
  44. package/dist/{cli-kpev657q.js → cli-y6b6fb76.js} +25 -3
  45. package/dist/{cli-123q2get.js → cli-yz3djrm0.js} +9 -9
  46. package/dist/{cli-wvp6bvy4.js → cli-zhjmqxzh.js} +1 -1
  47. package/dist/cli.js +13 -13
  48. package/dist/{codex-adapter-k98vaw2r.js → codex-adapter-zw4dwsx7.js} +2 -2
  49. package/dist/{codex-hook-9pf19cks.js → codex-hook-bw8p5nh4.js} +2 -2
  50. package/dist/{codex-session-runner-tngtphx7.js → codex-session-runner-k981s65v.js} +2 -2
  51. package/dist/{commands-y8nbr3fv.js → commands-sykrxx7e.js} +4 -4
  52. package/dist/{db-30p4h2t1.js → db-sfbsc8wt.js} +6 -4
  53. package/dist/handlers-0aspcejm.js +603 -0
  54. package/dist/{hook-dcw4jfbj.js → hook-4nvqj2sb.js} +7 -5
  55. package/dist/{hook-21jexvfx.js → hook-hxccs7m5.js} +24 -49
  56. package/dist/{http-r4kq5g8d.js → http-y06z517d.js} +230 -94
  57. package/dist/{index-q3qja6ta.js → index-hssww7kj.js} +15 -14
  58. package/dist/{index-gay66b9w.js → index-k8znfx0z.js} +13 -12
  59. package/dist/{index-53zyht4z.js → index-tce1rss4.js} +371 -110
  60. package/dist/{index-nqvf26za.js → index-x5j894zf.js} +14 -13
  61. package/dist/{keepalive-xs8fa55y.js → keepalive-w733ax66.js} +6 -6
  62. package/dist/{lead-d9m2t4s0.js → lead-75bjqa9d.js} +29 -27
  63. package/dist/{maintenance-3zh82g6z.js → maintenance-w4f1zjck.js} +8 -7
  64. package/dist/{oauth-refresh-sweep-03snsye5.js → oauth-refresh-sweep-wdetky26.js} +5 -5
  65. package/dist/{onboard-0cg1925x.js → onboard-bb5net8s.js} +2 -2
  66. package/dist/{otel-impl-0ab95z27.js → otel-impl-0w7c14ap.js} +1 -1
  67. package/dist/{pi-mono-adapter-dtkz4e7j.js → pi-mono-adapter-2has4gnp.js} +12 -2
  68. package/dist/{pricing-refresh-e5jhzz4j.js → pricing-refresh-9nzsrgcq.js} +6 -6
  69. package/dist/{rbac-roles-p1mf40z3.js → rbac-roles-r3hqjp09.js} +5 -5
  70. package/dist/{rbac-roles-2w3cmrkc.js → rbac-roles-z1jrffbt.js} +4 -4
  71. package/dist/{registry-9kdck5ye.js → registry-v5wytg0s.js} +1 -2
  72. package/dist/{render-v2-xhawry3k.js → render-v2-vbr89fnb.js} +6 -5
  73. package/dist/{seed-pricing-t223sw91.js → seed-pricing-g62hy1pk.js} +7 -5
  74. package/dist/{setup-zcs0rce7.js → setup-gnfqnptp.js} +2 -2
  75. package/dist/{worker-qnp2ka62.js → worker-kznxp4qm.js} +29 -27
  76. package/openapi.json +263 -12
  77. package/package.json +1 -1
  78. package/plugin/commands/start-worker.md +4 -9
  79. package/plugin/pi-skills/start-worker/SKILL.md +4 -9
  80. package/src/be/db/agents.ts +556 -0
  81. package/src/be/db/context-versions.ts +117 -0
  82. package/src/be/db/runtime.ts +406 -0
  83. package/src/be/db/tasks/read.ts +1167 -0
  84. package/src/be/db/tasks/write.ts +953 -0
  85. package/src/be/db.ts +584 -3429
  86. package/src/be/migrations/147_scheduled_tasks_parent_task.sql +7 -0
  87. package/src/be/migrations/148_pages_svg.sql +123 -0
  88. package/src/be/migrations/149_session_tokens.sql +24 -0
  89. package/src/be/scripts/typecheck.ts +1 -0
  90. package/src/be/seed-pricing.ts +13 -8
  91. package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
  92. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
  93. package/src/be/seed-scripts/catalog/star-history-refresh.ts +101 -0
  94. package/src/be/seed-scripts/catalog/star-history-renderer.ts +119 -0
  95. package/src/be/seed-scripts/index.ts +17 -0
  96. package/src/be/seed-skills/index.ts +14 -0
  97. package/src/be/swarm-config-guard.ts +5 -2
  98. package/src/be/task-attachment-store.ts +70 -0
  99. package/src/be/users.ts +77 -0
  100. package/src/commands/attachments-section.ts +37 -0
  101. package/src/commands/claude-managed-setup.ts +45 -3
  102. package/src/commands/claude-md-session.ts +408 -0
  103. package/src/commands/context-preamble.ts +15 -3
  104. package/src/commands/profile-sync.ts +205 -5
  105. package/src/commands/provider-credentials.ts +60 -27
  106. package/src/commands/runner.ts +63 -58
  107. package/src/commands/templates.ts +25 -2
  108. package/src/hooks/hook.ts +26 -67
  109. package/src/http/agents.ts +40 -1
  110. package/src/http/auth.ts +13 -1
  111. package/src/http/fs.ts +21 -51
  112. package/src/http/index.ts +2 -0
  113. package/src/http/mcp.ts +32 -0
  114. package/src/http/pages-public.ts +20 -4
  115. package/src/http/pages.ts +11 -3
  116. package/src/http/poll.ts +4 -0
  117. package/src/http/repos.ts +3 -3
  118. package/src/http/sessions.ts +82 -0
  119. package/src/http/tasks.ts +10 -2
  120. package/src/http/utils.ts +79 -1
  121. package/src/prompts/session-templates.ts +12 -8
  122. package/src/providers/acp-adapter.ts +62 -8
  123. package/src/providers/pi-mono-adapter.ts +23 -6
  124. package/src/providers/pricing-sources.md +11 -0
  125. package/src/scheduler/scheduler.test.ts +49 -2
  126. package/src/scheduler/scheduler.ts +4 -0
  127. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  128. package/src/scripts-runtime/types/stdlib.d.ts +8 -0
  129. package/src/scripts-runtime/types/swarm-sdk.d.ts +8 -0
  130. package/src/server.ts +2 -0
  131. package/src/slack/assistant.ts +87 -46
  132. package/src/slack/handlers.test.ts +1 -3
  133. package/src/slack/handlers.ts +102 -87
  134. package/src/slack/inbound-files.ts +466 -0
  135. package/src/slack/templates.ts +14 -0
  136. package/src/tasks/task-terminal-effects.ts +158 -0
  137. package/src/tests/acp-adapter.test.ts +369 -117
  138. package/src/tests/budget-claim-gate.test.ts +1 -0
  139. package/src/tests/build-pi-skills.test.ts +5 -7
  140. package/src/tests/claude-managed-setup.test.ts +52 -4
  141. package/src/tests/claude-md-session.test.ts +603 -0
  142. package/src/tests/codex-skill-resolver.test.ts +19 -0
  143. package/src/tests/context-versioning.test.ts +107 -0
  144. package/src/tests/cors-allowlist.test.ts +309 -0
  145. package/src/tests/credential-check.test.ts +157 -14
  146. package/src/tests/db-agents.test.ts +91 -0
  147. package/src/tests/db-context-versions.test.ts +37 -0
  148. package/src/tests/db-extraction-contract.test.ts +45 -0
  149. package/src/tests/db-runtime.test.ts +70 -0
  150. package/src/tests/db-task-read-contract.test.ts +624 -0
  151. package/src/tests/db-task-write-contract.test.ts +653 -0
  152. package/src/tests/db-tasks-read.test.ts +183 -0
  153. package/src/tests/db-tasks-write.test.ts +230 -0
  154. package/src/tests/defer-task.test.ts +389 -0
  155. package/src/tests/docs-index-footprint.test.js +2 -2
  156. package/src/tests/e2e-visuals-stage.test.ts +151 -0
  157. package/src/tests/file-lock.test.ts +144 -0
  158. package/src/tests/fixtures/file-lock-holder.ts +13 -0
  159. package/src/tests/fixtures/hold-file-lock.ts +25 -0
  160. package/src/tests/heartbeat-expiry.test.ts +188 -0
  161. package/src/tests/http-api-integration.test.ts +78 -1
  162. package/src/tests/pages-public-html.test.ts +43 -0
  163. package/src/tests/pages-svg-migration.test.ts +42 -0
  164. package/src/tests/preload.ts +4 -1
  165. package/src/tests/profile-sync-warning.test.ts +16 -0
  166. package/src/tests/profile-sync.test.ts +126 -0
  167. package/src/tests/prompt-template-session.test.ts +20 -4
  168. package/src/tests/provider-registration.test.ts +93 -0
  169. package/src/tests/rbac-e2e-helpers.test.ts +53 -0
  170. package/src/tests/rbac-e2e-helpers.ts +19 -2
  171. package/src/tests/reload-config.test.ts +26 -0
  172. package/src/tests/routing-affinity-inventory.test.ts +1 -1
  173. package/src/tests/runner-attachments-section.test.ts +22 -2
  174. package/src/tests/runner-context-preamble.test.ts +30 -0
  175. package/src/tests/runner-deployment-resume.test.ts +107 -0
  176. package/src/tests/runner-repo-autostash.test.ts +12 -0
  177. package/src/tests/script-connections.test.ts +4 -0
  178. package/src/tests/seed-scripts.test.ts +2 -2
  179. package/src/tests/session-tokens.test.ts +279 -0
  180. package/src/tests/slack-file-tools-attachments.test.ts +408 -0
  181. package/src/tests/slack-inbound-files.test.ts +847 -0
  182. package/src/tests/star-history-refresh.test.ts +102 -0
  183. package/src/tests/swarm-config-reserved-keys.test.ts +21 -0
  184. package/src/tests/swarm-repos.test.ts +20 -0
  185. package/src/tests/tool-annotations.test.ts +4 -3
  186. package/src/tests/ui-logs-parser.test.ts +176 -0
  187. package/src/tools/create-page.ts +4 -2
  188. package/src/tools/defer-task.ts +239 -0
  189. package/src/tools/repos/update-repo.ts +2 -2
  190. package/src/tools/schedules/create-schedule.ts +1 -0
  191. package/src/tools/schedules/list-schedules.ts +1 -0
  192. package/src/tools/schedules/patch-schedule.ts +1 -0
  193. package/src/tools/schedules/update-schedule.ts +1 -0
  194. package/src/tools/slack-download-file.ts +60 -29
  195. package/src/tools/slack-read.ts +73 -32
  196. package/src/tools/store-progress.ts +16 -146
  197. package/src/tools/tool-config.ts +1 -0
  198. package/src/tools/utils.ts +21 -0
  199. package/src/types.ts +37 -4
  200. package/src/utils/acp-session-token.ts +56 -0
  201. package/src/utils/file-lock.ts +111 -0
  202. package/src/utils/heartbeat-expiry.ts +78 -0
  203. package/src/utils/request-auth-context.ts +2 -0
  204. package/src/utils/task-attachment-links.ts +22 -0
  205. package/templates/skills/code-quality/content.md +8 -5
  206. package/templates/skills/download-task-attachment/content.md +2 -1
  207. package/{plugin/pi-skills → templates/skills}/review-offered-task/SKILL.md +5 -5
  208. package/templates/skills/review-offered-task/config.json +14 -0
  209. package/{plugin/commands/review-offered-task.md → templates/skills/review-offered-task/content.md} +3 -8
  210. package/templates/skills/scheduling/content.md +4 -0
  211. package/templates/skills/slack-interaction/content.md +2 -2
  212. package/templates/skills/work-on-task/SKILL.md +16 -0
  213. package/templates/skills/work-on-task/config.json +14 -0
  214. package/templates/skills/work-on-task/content.md +11 -0
  215. package/dist/assistant-0qt42nzg.js +0 -212
  216. package/dist/cli-fs1kkqgr.js +0 -317
  217. package/dist/cli-jnf5zghe.js +0 -269
  218. package/dist/cli-n1q8q14d.js +0 -177
  219. package/dist/handlers-yfp1qn81.js +0 -592
  220. package/plugin/commands/work-on-task.md +0 -39
  221. package/plugin/pi-skills/work-on-task/SKILL.md +0 -39
@@ -2,10 +2,17 @@ import { Assistant } from "@slack/bolt";
2
2
  import { getAgentWorkingOnThread, getLeadAgent, getMostRecentTaskInThread } from "../be/db";
3
3
  import { resolveTemplate } from "../prompts/resolver";
4
4
  import { slackContextKey } from "../tasks/context-key";
5
- import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
6
5
  import { ackSlackMessage, reactionName } from "./ack";
7
6
  import { resolveSlackUserId, rewriteSlackMentions } from "./enrich";
8
7
  import { wasEventSeen } from "./event-dedup";
8
+ import type { SlackFile } from "./files";
9
+ import {
10
+ bufferedFileFailures,
11
+ buildEffectiveText,
12
+ createSlackTaskWithFiles,
13
+ fetchSlackFiles,
14
+ notifySlackFileFailures,
15
+ } from "./inbound-files";
9
16
  import { ensureSlackThreadTree, isSlackRenderV2Enabled } from "./render-v2";
10
17
  import { hasOtherUserMention } from "./router";
11
18
  import { bufferThreadMessage, getBufferMessageCount } from "./thread-buffer";
@@ -80,12 +87,10 @@ export function createAssistant(): Assistant {
80
87
  const threadTs = (msg.thread_ts as string) || message.ts;
81
88
  const channelId = message.channel;
82
89
  const messageText = (msg.text as string) || "";
90
+ // `file_share` messages land here too: an image sent with no caption
91
+ // arrives with empty `text` and everything in `files`.
92
+ const files = msg.files as SlackFile[] | undefined;
83
93
  const userId = (msg.user as string) || "";
84
- // Any in-body `<@U…>` mention the requester typed is rewritten via
85
- // the identity primitive before it reaches agent-visible task text —
86
- // never a raw Slack ID. Bot-mention routing checks below use the
87
- // raw `messageText`, not this rendered copy.
88
- const renderedMessageText = await rewriteSlackMentions(messageText);
89
94
 
90
95
  // Resolve the bot's own Slack user ID (cached after first call) so we can
91
96
  // check whether this message is actually addressed to us.
@@ -123,30 +128,55 @@ export function createAssistant(): Assistant {
123
128
  // 1. Check if an agent is already working in this thread
124
129
  const workingAgent = await getAgentWorkingOnThread(channelId, threadTs);
125
130
 
126
- if (workingAgent && workingAgent.status !== "offline") {
127
- // Follow-up message → route to the same agent
128
- if (isAdditiveSlack()) {
129
- bufferThreadMessage(channelId, threadTs, messageText, userId, message.ts);
130
- const count = getBufferMessageCount(`${channelId}:${threadTs}`);
131
- const event = count === 1 ? "accepted" : "buffered";
132
- await ackSlackMessage(client, channelId, message.ts, reactionName(event), event);
133
- await safeSetStatus("Queuing follow-up...");
134
- return;
135
- }
131
+ // Follow-up message → route to the same agent. Buffered follow-ups
132
+ // carry the `[File: …]` lines only; their files are not attached.
133
+ if (workingAgent && workingAgent.status !== "offline" && isAdditiveSlack()) {
134
+ const unattached = bufferedFileFailures(files);
135
+ bufferThreadMessage(
136
+ channelId,
137
+ threadTs,
138
+ buildEffectiveText(messageText, files, unattached),
139
+ userId,
140
+ message.ts,
141
+ );
142
+ const count = getBufferMessageCount(`${channelId}:${threadTs}`);
143
+ const event = count === 1 ? "accepted" : "buffered";
144
+ await ackSlackMessage(client, channelId, message.ts, reactionName(event), event);
145
+ await notifySlackFileFailures(client, channelId, threadTs, unattached);
146
+ await safeSetStatus("Queuing follow-up...");
147
+ return;
148
+ }
149
+
150
+ // Fetch shared files before the task text is final: a file that can't be
151
+ // downloaded is flagged in it.
152
+ if (files?.length) await safeSetStatus("Downloading attachments...");
153
+ await using inbound = await fetchSlackFiles(client, files);
154
+ // Any in-body `<@U…>` mention the requester typed is rewritten via
155
+ // the identity primitive before it reaches agent-visible task text —
156
+ // never a raw Slack ID. Bot-mention routing checks above use the
157
+ // raw `messageText`, not this rendered copy.
158
+ const renderedMessageText = await rewriteSlackMentions(
159
+ buildEffectiveText(messageText, inbound.files, inbound.failed),
160
+ );
136
161
 
162
+ if (workingAgent && workingAgent.status !== "offline") {
137
163
  // Otherwise, create a follow-up task for the working agent
138
164
  const latestTask = await getMostRecentTaskInThread(channelId, threadTs);
139
- const task = await createTaskWithSiblingAwareness(renderedMessageText, {
140
- agentId: workingAgent.id,
141
- source: "slack",
142
- slackChannelId: channelId,
143
- slackThreadTs: threadTs,
144
- slackTriggerMessageTs: message.ts,
145
- slackUserId: userId,
146
- parentTaskId: latestTask?.id,
147
- requestedByUserId,
148
- contextKey: slackContextKey({ channelId, threadTs }),
149
- });
165
+ const { task, unattached } = await createSlackTaskWithFiles(
166
+ renderedMessageText,
167
+ {
168
+ agentId: workingAgent.id,
169
+ source: "slack",
170
+ slackChannelId: channelId,
171
+ slackThreadTs: threadTs,
172
+ slackTriggerMessageTs: message.ts,
173
+ slackUserId: userId,
174
+ parentTaskId: latestTask?.id,
175
+ requestedByUserId,
176
+ contextKey: slackContextKey({ channelId, threadTs }),
177
+ },
178
+ inbound,
179
+ );
150
180
  await ackSlackMessage(
151
181
  client,
152
182
  channelId,
@@ -154,6 +184,7 @@ export function createAssistant(): Assistant {
154
184
  reactionName("accepted"),
155
185
  "accepted",
156
186
  );
187
+ await notifySlackFileFailures(client, channelId, threadTs, unattached);
157
188
 
158
189
  if (isSlackRenderV2Enabled()) await ensureSlackThreadTree([task.id]);
159
190
 
@@ -182,15 +213,19 @@ export function createAssistant(): Assistant {
182
213
  const lead = await getLeadAgent();
183
214
  if (!lead) {
184
215
  // No lead — still queue the task
185
- const task = await createTaskWithSiblingAwareness(renderedMessageText + channelContext, {
186
- source: "slack",
187
- slackChannelId: channelId,
188
- slackThreadTs: threadTs,
189
- slackTriggerMessageTs: message.ts,
190
- slackUserId: userId,
191
- requestedByUserId,
192
- contextKey: slackContextKey({ channelId, threadTs }),
193
- });
216
+ const { task, unattached } = await createSlackTaskWithFiles(
217
+ renderedMessageText + channelContext,
218
+ {
219
+ source: "slack",
220
+ slackChannelId: channelId,
221
+ slackThreadTs: threadTs,
222
+ slackTriggerMessageTs: message.ts,
223
+ slackUserId: userId,
224
+ requestedByUserId,
225
+ contextKey: slackContextKey({ channelId, threadTs }),
226
+ },
227
+ inbound,
228
+ );
194
229
  await ackSlackMessage(
195
230
  client,
196
231
  channelId,
@@ -198,6 +233,7 @@ export function createAssistant(): Assistant {
198
233
  reactionName("accepted"),
199
234
  "accepted",
200
235
  );
236
+ await notifySlackFileFailures(client, channelId, threadTs, unattached);
201
237
  if (isSlackRenderV2Enabled()) {
202
238
  await ensureSlackThreadTree([task.id]);
203
239
  } else {
@@ -207,17 +243,22 @@ export function createAssistant(): Assistant {
207
243
  return;
208
244
  }
209
245
 
210
- const task = await createTaskWithSiblingAwareness(renderedMessageText + channelContext, {
211
- agentId: lead.id,
212
- source: "slack",
213
- slackChannelId: channelId,
214
- slackThreadTs: threadTs,
215
- slackTriggerMessageTs: message.ts,
216
- slackUserId: userId,
217
- requestedByUserId,
218
- contextKey: slackContextKey({ channelId, threadTs }),
219
- });
246
+ const { task, unattached } = await createSlackTaskWithFiles(
247
+ renderedMessageText + channelContext,
248
+ {
249
+ agentId: lead.id,
250
+ source: "slack",
251
+ slackChannelId: channelId,
252
+ slackThreadTs: threadTs,
253
+ slackTriggerMessageTs: message.ts,
254
+ slackUserId: userId,
255
+ requestedByUserId,
256
+ contextKey: slackContextKey({ channelId, threadTs }),
257
+ },
258
+ inbound,
259
+ );
220
260
  await ackSlackMessage(client, channelId, message.ts, reactionName("accepted"), "accepted");
261
+ await notifySlackFileFailures(client, channelId, threadTs, unattached);
221
262
  if (isSlackRenderV2Enabled()) await ensureSlackThreadTree([task.id]);
222
263
  // setStatus shows typing indicator — watcher will post final result when done
223
264
  } catch (error) {
@@ -17,15 +17,13 @@ import { createAssistant } from "./assistant";
17
17
  import * as slackEnrichModule from "./enrich";
18
18
  import type { SlackFile } from "./files";
19
19
  import {
20
- buildAttachmentText,
21
- buildEffectiveText,
22
20
  checkUserAccess,
23
- formatFileSize,
24
21
  isBotMessage,
25
22
  isSwarmThreadRoot,
26
23
  registerMessageHandler,
27
24
  type UserFilterConfig,
28
25
  } from "./handlers";
26
+ import { buildAttachmentText, buildEffectiveText, formatFileSize } from "./inbound-files";
29
27
  import { SLACK_REACTION_CONFIG_KEYS } from "./reaction-shortcode";
30
28
 
31
29
  describe("checkUserAccess", () => {
@@ -9,13 +9,20 @@ import {
9
9
  } from "../be/db";
10
10
  import { resolveTemplate } from "../prompts/resolver";
11
11
  import { slackContextKey } from "../tasks/context-key";
12
- import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
13
12
  import { workflowEventBus } from "../workflows/event-bus";
14
13
  import { ackSlackMessage, reactionName } from "./ack";
15
14
  import { buildTreeBlocks, type TreeNode } from "./blocks";
16
15
  import { enrichSlackUserEmail, resolveSlackUserId, rewriteSlackMentions } from "./enrich";
17
16
  import { wasEventSeen } from "./event-dedup";
18
17
  import type { SlackFile } from "./files";
18
+ import {
19
+ bufferedFileFailures,
20
+ buildEffectiveText,
21
+ createSlackTaskWithFiles,
22
+ fetchSlackFiles,
23
+ notifySlackFileFailures,
24
+ type SlackFileFailure,
25
+ } from "./inbound-files";
19
26
  import { extractTaskFromMessage, hasOtherUserMention, routeMessage } from "./router";
20
27
  // Side-effect import: registers all Slack event templates in the in-memory registry
21
28
  import "./templates";
@@ -336,45 +343,6 @@ async function getThreadContext(
336
343
  }
337
344
  }
338
345
 
339
- /**
340
- * Format a file size in bytes to a human-readable string.
341
- */
342
- export function formatFileSize(bytes: number): string {
343
- if (bytes < 1024) return `${bytes} B`;
344
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
345
- if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
346
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
347
- }
348
-
349
- /**
350
- * Build a text representation of file attachments for inclusion in messages.
351
- * Each file is formatted as: [File: filename.ext (mimetype, size) id=FILE_ID]
352
- */
353
- export function buildAttachmentText(files: SlackFile[]): string {
354
- return files
355
- .map((f) => `[File: ${f.name} (${f.mimetype}, ${formatFileSize(f.size)}) id=${f.id}]`)
356
- .join("\n");
357
- }
358
-
359
- /**
360
- * Build the effective message text from the original text and any file attachments.
361
- * - Text only: returns the text as-is
362
- * - Files only: returns the attachment metadata
363
- * - Both: returns text followed by attachment metadata
364
- */
365
- export function buildEffectiveText(text: string | undefined, files?: SlackFile[]): string {
366
- const hasText = !!text?.trim();
367
- const hasFiles = files && files.length > 0;
368
-
369
- if (hasText && hasFiles) {
370
- return `${text}\n\n${buildAttachmentText(files)}`;
371
- }
372
- if (hasFiles) {
373
- return buildAttachmentText(files);
374
- }
375
- return text || "";
376
- }
377
-
378
346
  // Message deduplication (prevents duplicate event processing)
379
347
  const processedMessages = new Set<string>();
380
348
  const MESSAGE_DEDUP_TTL = 60_000; // 1 minute
@@ -501,9 +469,11 @@ export function registerMessageHandler(app: App): void {
501
469
  // Check if bot was mentioned (in original text only)
502
470
  const botMentioned = !!msg.text?.includes(`<@${botUserId}>`);
503
471
 
504
- // Detect assistant thread context — file_share messages in DM assistant threads
505
- // bypass the assistant handler and land here instead. Treat them as implicit mentions
506
- // so they route to the lead agent rather than being silently dropped.
472
+ // Detect assistant thread context. Bolt's Assistant middleware claims DM thread
473
+ // messages with no subtype or `file_share` (`isAssistantMessage` in @slack/bolt), so
474
+ // those — file uploads included — are handled in assistant.ts. An assistant-thread
475
+ // message that still lands here is treated as an implicit mention so it routes to
476
+ // the lead agent rather than being silently dropped.
507
477
  // Guard: suppress implicit mention when the message @-mentions someone else but NOT us —
508
478
  // those messages are addressed to a different agent/user (e.g. Devin) and must not spawn.
509
479
  const isAssistantThread = !!msg.assistant_thread;
@@ -517,7 +487,11 @@ export function registerMessageHandler(app: App): void {
517
487
  false,
518
488
  );
519
489
  if (additiveSlack && msg.thread_ts) {
520
- const stripped = effectiveText.replace(/<@[A-Z0-9]+>/g, "").trim();
490
+ // A !now follow-up is buffered too: its files ride along as marked `[File: …]` lines only.
491
+ const unattached = bufferedFileFailures(msg.files);
492
+ const stripped = buildEffectiveText(msg.text, msg.files, unattached)
493
+ .replace(/<@[A-Z0-9]+>/g, "")
494
+ .trim();
521
495
  if (
522
496
  stripped.startsWith("!now") &&
523
497
  (await hasSwarmThreadActivity(client, msg.channel, msg.thread_ts))
@@ -537,6 +511,7 @@ export function registerMessageHandler(app: App): void {
537
511
  await instantFlush(threadKey);
538
512
 
539
513
  await ackSlackMessage(client, msg.channel, msg.ts, reactionName("now"), "now");
514
+ await notifySlackFileFailures(client, msg.channel, msg.thread_ts, unattached);
540
515
 
541
516
  return;
542
517
  }
@@ -563,7 +538,15 @@ export function registerMessageHandler(app: App): void {
563
538
 
564
539
  if (hasSwarmActivity) {
565
540
  const threadKey = `${msg.channel}:${msg.thread_ts}`;
566
- bufferThreadMessage(msg.channel, msg.thread_ts, effectiveText, msg.user, msg.ts);
541
+ // Buffered follow-ups carry the `[File: …]` lines only; their files are not attached.
542
+ const unattached = bufferedFileFailures(msg.files);
543
+ bufferThreadMessage(
544
+ msg.channel,
545
+ msg.thread_ts,
546
+ buildEffectiveText(msg.text, msg.files, unattached),
547
+ msg.user,
548
+ msg.ts,
549
+ );
567
550
 
568
551
  // Slack feedback: react with the accepted reaction on first buffer, buffered on appends
569
552
  const count = getBufferMessageCount(threadKey);
@@ -571,6 +554,7 @@ export function registerMessageHandler(app: App): void {
571
554
  const name = reactionName(event);
572
555
  console.log(`[Slack] Additive buffer: ${threadKey} (message #${count}, reaction: ${name})`);
573
556
  await ackSlackMessage(client, msg.channel, msg.ts, name, event);
557
+ await notifySlackFileFailures(client, msg.channel, msg.thread_ts, unattached);
574
558
 
575
559
  return; // Don't process further — buffer will flush
576
560
  }
@@ -602,9 +586,11 @@ export function registerMessageHandler(app: App): void {
602
586
  return;
603
587
  }
604
588
 
589
+ await using inbound = await fetchSlackFiles(client, msg.files);
590
+ const taskText = buildEffectiveText(msg.text, inbound.files, inbound.failed);
605
591
  const taskDescription = isImplicitMention
606
- ? effectiveText
607
- : extractTaskFromMessage(effectiveText, botUserId);
592
+ ? taskText
593
+ : extractTaskFromMessage(taskText, botUserId);
608
594
  if (!taskDescription) {
609
595
  if (!isImplicitMention && !isSlackRenderV2Enabled()) {
610
596
  await say({
@@ -634,17 +620,22 @@ export function registerMessageHandler(app: App): void {
634
620
  }
635
621
 
636
622
  const lead = await getLeadAgent();
637
- const task = await createTaskWithSiblingAwareness(fullTaskDescription, {
638
- agentId: lead?.id,
639
- source: "slack",
640
- slackChannelId: msg.channel,
641
- slackThreadTs: threadTs,
642
- slackTriggerMessageTs: msg.ts,
643
- slackUserId: msg.user,
644
- requestedByUserId,
645
- contextKey: slackContextKey({ channelId: msg.channel, threadTs }),
646
- });
623
+ const { task, unattached } = await createSlackTaskWithFiles(
624
+ fullTaskDescription,
625
+ {
626
+ agentId: lead?.id,
627
+ source: "slack",
628
+ slackChannelId: msg.channel,
629
+ slackThreadTs: threadTs,
630
+ slackTriggerMessageTs: msg.ts,
631
+ slackUserId: msg.user,
632
+ requestedByUserId,
633
+ contextKey: slackContextKey({ channelId: msg.channel, threadTs }),
634
+ },
635
+ inbound,
636
+ );
647
637
  await ackSlackMessage(client, msg.channel, msg.ts, reactionName("accepted"), "accepted");
638
+ await notifySlackFileFailures(client, msg.channel, threadTs, unattached);
648
639
 
649
640
  if (isSlackRenderV2Enabled()) {
650
641
  await ensureSlackThreadTree([task.id]);
@@ -668,8 +659,12 @@ export function registerMessageHandler(app: App): void {
668
659
  return;
669
660
  }
670
661
 
671
- // Extract task description (using effective text which includes attachment metadata)
672
- const taskDescription = extractTaskFromMessage(effectiveText, botUserId);
662
+ // Extract task description (the text plus a `[File: …]` line per attachment)
663
+ await using inbound = await fetchSlackFiles(client, msg.files);
664
+ const taskDescription = extractTaskFromMessage(
665
+ buildEffectiveText(msg.text, inbound.files, inbound.failed),
666
+ botUserId,
667
+ );
673
668
  if (!taskDescription) {
674
669
  if (!isSlackRenderV2Enabled()) {
675
670
  await say({
@@ -706,6 +701,7 @@ export function registerMessageHandler(app: App): void {
706
701
  steered: Array<{ agentName: string; acknowledgement: string }>;
707
702
  failed: Array<{ agentName: string; reason: string }>;
708
703
  } = { assigned: [], queued: [], steered: [], failed: [] };
704
+ const unattached: SlackFileFailure[] = [];
709
705
 
710
706
  for (const match of matches) {
711
707
  const agent = await getAgentById(match.agent.id);
@@ -718,15 +714,18 @@ export function registerMessageHandler(app: App): void {
718
714
  try {
719
715
  const latestTask = await getMostRecentTaskInThread(msg.channel, threadTs);
720
716
  if (agent.isLead) {
721
- const steering = msg.thread_ts
722
- ? await requestSlackThreadSteering({
723
- channelId: msg.channel,
724
- threadTs,
725
- message: taskDescription,
726
- messageTimestamps: [msg.ts],
727
- requestedByUserId,
728
- })
729
- : null;
717
+ // Steering only carries text into the running session; a message with
718
+ // files becomes a follow-up task so the files land as attachments.
719
+ const steering =
720
+ msg.thread_ts && inbound.files.length === 0
721
+ ? await requestSlackThreadSteering({
722
+ channelId: msg.channel,
723
+ threadTs,
724
+ message: taskDescription,
725
+ messageTimestamps: [msg.ts],
726
+ requestedByUserId,
727
+ })
728
+ : null;
730
729
  if (steering) {
731
730
  await ackSlackMessage(client, msg.channel, msg.ts, reactionName("steered"), "steered");
732
731
  results.steered.push({
@@ -736,33 +735,45 @@ export function registerMessageHandler(app: App): void {
736
735
  continue;
737
736
  }
738
737
 
739
- const task = await createTaskWithSiblingAwareness(fullTaskDescription, {
738
+ const created = await createSlackTaskWithFiles(
739
+ fullTaskDescription,
740
+ {
741
+ agentId: agent.id,
742
+ source: "slack",
743
+ slackChannelId: msg.channel,
744
+ slackThreadTs: threadTs,
745
+ slackTriggerMessageTs: msg.ts,
746
+ slackUserId: msg.user,
747
+ parentTaskId: latestTask?.id,
748
+ requestedByUserId,
749
+ contextKey: slackContextKey({ channelId: msg.channel, threadTs }),
750
+ },
751
+ inbound,
752
+ );
753
+ const { task } = created;
754
+ unattached.push(...created.unattached);
755
+ await ackSlackMessage(client, msg.channel, msg.ts, reactionName("accepted"), "accepted");
756
+ results.assigned.push({ agentName: agent.name, taskId: task.id });
757
+ continue;
758
+ }
759
+
760
+ // Workers receive tasks as before
761
+ const created = await createSlackTaskWithFiles(
762
+ fullTaskDescription,
763
+ {
740
764
  agentId: agent.id,
741
765
  source: "slack",
742
766
  slackChannelId: msg.channel,
743
767
  slackThreadTs: threadTs,
744
768
  slackTriggerMessageTs: msg.ts,
745
769
  slackUserId: msg.user,
746
- parentTaskId: latestTask?.id,
747
770
  requestedByUserId,
748
771
  contextKey: slackContextKey({ channelId: msg.channel, threadTs }),
749
- });
750
- await ackSlackMessage(client, msg.channel, msg.ts, reactionName("accepted"), "accepted");
751
- results.assigned.push({ agentName: agent.name, taskId: task.id });
752
- continue;
753
- }
754
-
755
- // Workers receive tasks as before
756
- const task = await createTaskWithSiblingAwareness(fullTaskDescription, {
757
- agentId: agent.id,
758
- source: "slack",
759
- slackChannelId: msg.channel,
760
- slackThreadTs: threadTs,
761
- slackTriggerMessageTs: msg.ts,
762
- slackUserId: msg.user,
763
- requestedByUserId,
764
- contextKey: slackContextKey({ channelId: msg.channel, threadTs }),
765
- });
772
+ },
773
+ inbound,
774
+ );
775
+ const { task } = created;
776
+ unattached.push(...created.unattached);
766
777
  await ackSlackMessage(client, msg.channel, msg.ts, reactionName("accepted"), "accepted");
767
778
 
768
779
  // Check if agent has an in-progress task in this thread (queued follow-up)
@@ -781,6 +792,10 @@ export function registerMessageHandler(app: App): void {
781
792
  }
782
793
  }
783
794
 
795
+ if (results.assigned.length + results.queued.length > 0) {
796
+ await notifySlackFileFailures(client, msg.channel, threadTs, unattached);
797
+ }
798
+
784
799
  // Send consolidated summary as initial tree with Block Kit
785
800
  const totalResults = results.assigned.length + results.queued.length + results.failed.length;
786
801
  if (totalResults > 0) {