@desplega.ai/agent-swarm 1.146.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 (214) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-adapter-jnq4612m.js → acp-adapter-9n319wqc.js} +42 -4
  3. package/dist/{actions-s41xcn3f.js → actions-2vxqvpr9.js} +9 -8
  4. package/dist/{app-j8bfw3f6.js → app-w066xfy0.js} +6 -5
  5. package/dist/assistant-kt31dj2h.js +236 -0
  6. package/dist/{boot-reembed-jxdz46jp.js → boot-reembed-az5rassp.js} +7 -6
  7. package/dist/{boot-reembed-x1476agm.js → boot-reembed-j3mm3rfz.js} +8 -7
  8. package/dist/{boot-scrub-logs-rab9h5bt.js → boot-scrub-logs-b6h54817.js} +4 -4
  9. package/dist/{claude-adapter-ep6qekbm.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-0wp3qrcy.js → claude-sdk-session-f0cc1kec.js} +8 -6
  12. package/dist/{cli-bafd6xnf.js → cli-0b6y7kxm.js} +1 -1
  13. package/dist/{cli-jyy3e193.js → cli-12fz972k.js} +4 -4
  14. package/dist/{cli-pzfc7j7g.js → cli-1th1728g.js} +2 -2
  15. package/dist/{cli-68h2mfrn.js → cli-3tgwnf43.js} +3 -3
  16. package/dist/{cli-q3qr220d.js → cli-4nr988h6.js} +1 -1
  17. package/dist/{cli-m4xsy03s.js → cli-53s590z8.js} +1 -1
  18. package/dist/{cli-c8q7292f.js → cli-54wwve05.js} +5 -4
  19. package/dist/{cli-rymrjneb.js → cli-719k9j2c.js} +2 -2
  20. package/dist/{cli-zn65x0w8.js → cli-89xfd7f9.js} +21 -47
  21. package/dist/{cli-506z6jqa.js → cli-8v0g1yc8.js} +1 -1
  22. package/dist/cli-9d8ntetj.js +42 -0
  23. package/dist/{cli-dhg6j8h3.js → cli-atcve9y8.js} +826 -737
  24. package/dist/{cli-46yck6nj.js → cli-b5bq9crk.js} +14 -31
  25. package/dist/cli-d8brzxjb.js +190 -0
  26. package/dist/{cli-a960nkfb.js → cli-de14znh8.js} +2 -2
  27. package/dist/{cli-s8sbe32s.js → cli-dttph1bw.js} +2 -2
  28. package/dist/cli-ep19k3mb.js +19 -0
  29. package/dist/{cli-d9pnxgth.js → cli-f146mn5s.js} +7 -50
  30. package/dist/{cli-qar1f940.js → cli-f7dsy61y.js} +5 -3
  31. package/dist/{cli-d4xqde2b.js → cli-ftht3zzj.js} +2780 -2587
  32. package/dist/{cli-pmwbdw36.js → cli-hbv0kq6w.js} +2 -2
  33. package/dist/{cli-fhsz3twr.js → cli-jsp14b6c.js} +171 -1
  34. package/dist/{cli-2zvtzevh.js → cli-kpv03zhj.js} +13 -11
  35. package/dist/{cli-a9bn9svs.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-6xwhtc64.js → cli-n8508nre.js} +1 -1
  39. package/dist/{cli-q0qq1sxw.js → cli-p1d5b073.js} +37 -2
  40. package/dist/{cli-yqeb27t1.js → cli-rw51bq3j.js} +13 -10
  41. package/dist/{cli-zww3vcnm.js → cli-smby5m17.js} +7 -6
  42. package/dist/{cli-ah5hjk49.js → cli-tzvk9haz.js} +94 -372
  43. package/dist/{cli-yxwe913z.js → cli-w9kb3bk1.js} +1 -1
  44. package/dist/{cli-skd674mt.js → cli-y6b6fb76.js} +25 -3
  45. package/dist/{cli-3k2g3kg2.js → cli-yz3djrm0.js} +9 -9
  46. package/dist/{cli-4secrq32.js → cli-zhjmqxzh.js} +1 -1
  47. package/dist/cli.js +13 -13
  48. package/dist/{codex-adapter-d6qajxj3.js → codex-adapter-zw4dwsx7.js} +2 -2
  49. package/dist/{codex-hook-n7m5k7cr.js → codex-hook-bw8p5nh4.js} +2 -2
  50. package/dist/{codex-session-runner-v9krqe4x.js → codex-session-runner-k981s65v.js} +2 -2
  51. package/dist/{commands-923wsas7.js → commands-sykrxx7e.js} +4 -4
  52. package/dist/{db-m1hy4zh5.js → db-sfbsc8wt.js} +6 -4
  53. package/dist/handlers-0aspcejm.js +603 -0
  54. package/dist/{hook-ee4hff56.js → hook-4nvqj2sb.js} +7 -5
  55. package/dist/{hook-bxe3bkdy.js → hook-hxccs7m5.js} +24 -49
  56. package/dist/{http-a8rx91me.js → http-y06z517d.js} +223 -92
  57. package/dist/{index-697zfzv1.js → index-hssww7kj.js} +15 -14
  58. package/dist/{index-3x9g3nbg.js → index-k8znfx0z.js} +13 -12
  59. package/dist/{index-1j9va48r.js → index-tce1rss4.js} +371 -110
  60. package/dist/{index-jj4q3x59.js → index-x5j894zf.js} +14 -13
  61. package/dist/{keepalive-sds4e4ce.js → keepalive-w733ax66.js} +6 -6
  62. package/dist/{lead-ggq6szk8.js → lead-75bjqa9d.js} +29 -27
  63. package/dist/{maintenance-093p41d4.js → maintenance-w4f1zjck.js} +8 -7
  64. package/dist/{oauth-refresh-sweep-dvzq4ckj.js → oauth-refresh-sweep-wdetky26.js} +5 -5
  65. package/dist/{onboard-ydr9tzw5.js → onboard-bb5net8s.js} +2 -2
  66. package/dist/{otel-impl-zx3tdf3j.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-7e6pqnzp.js → pricing-refresh-9nzsrgcq.js} +6 -6
  69. package/dist/{rbac-roles-p2rh6d0r.js → rbac-roles-r3hqjp09.js} +5 -5
  70. package/dist/{rbac-roles-0dv1wt0w.js → rbac-roles-z1jrffbt.js} +4 -4
  71. package/dist/{registry-9kdck5ye.js → registry-v5wytg0s.js} +1 -2
  72. package/dist/{render-v2-4k3jt4r2.js → render-v2-vbr89fnb.js} +6 -5
  73. package/dist/{seed-pricing-2cc1qmcd.js → seed-pricing-g62hy1pk.js} +7 -5
  74. package/dist/{setup-c5yss30k.js → setup-gnfqnptp.js} +2 -2
  75. package/dist/{worker-7srgeqcr.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 +583 -3428
  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 +1 -1
  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/mcp.ts +32 -0
  113. package/src/http/pages-public.ts +20 -4
  114. package/src/http/poll.ts +4 -0
  115. package/src/http/repos.ts +3 -3
  116. package/src/http/sessions.ts +82 -0
  117. package/src/http/tasks.ts +10 -2
  118. package/src/prompts/session-templates.ts +4 -2
  119. package/src/providers/acp-adapter.ts +42 -6
  120. package/src/providers/pi-mono-adapter.ts +23 -6
  121. package/src/scheduler/scheduler.test.ts +49 -2
  122. package/src/scheduler/scheduler.ts +4 -0
  123. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  124. package/src/scripts-runtime/types/stdlib.d.ts +8 -0
  125. package/src/scripts-runtime/types/swarm-sdk.d.ts +8 -0
  126. package/src/server.ts +2 -0
  127. package/src/slack/assistant.ts +87 -46
  128. package/src/slack/handlers.test.ts +1 -3
  129. package/src/slack/handlers.ts +102 -87
  130. package/src/slack/inbound-files.ts +466 -0
  131. package/src/slack/templates.ts +14 -0
  132. package/src/tasks/task-terminal-effects.ts +158 -0
  133. package/src/tests/acp-adapter.test.ts +327 -192
  134. package/src/tests/budget-claim-gate.test.ts +1 -0
  135. package/src/tests/build-pi-skills.test.ts +5 -7
  136. package/src/tests/claude-managed-setup.test.ts +52 -4
  137. package/src/tests/claude-md-session.test.ts +603 -0
  138. package/src/tests/codex-skill-resolver.test.ts +19 -0
  139. package/src/tests/context-versioning.test.ts +107 -0
  140. package/src/tests/credential-check.test.ts +157 -14
  141. package/src/tests/db-agents.test.ts +91 -0
  142. package/src/tests/db-context-versions.test.ts +37 -0
  143. package/src/tests/db-extraction-contract.test.ts +45 -0
  144. package/src/tests/db-runtime.test.ts +70 -0
  145. package/src/tests/db-task-read-contract.test.ts +624 -0
  146. package/src/tests/db-task-write-contract.test.ts +653 -0
  147. package/src/tests/db-tasks-read.test.ts +183 -0
  148. package/src/tests/db-tasks-write.test.ts +230 -0
  149. package/src/tests/defer-task.test.ts +389 -0
  150. package/src/tests/docs-index-footprint.test.js +2 -2
  151. package/src/tests/e2e-visuals-stage.test.ts +151 -0
  152. package/src/tests/file-lock.test.ts +144 -0
  153. package/src/tests/fixtures/file-lock-holder.ts +13 -0
  154. package/src/tests/fixtures/hold-file-lock.ts +25 -0
  155. package/src/tests/heartbeat-expiry.test.ts +188 -0
  156. package/src/tests/http-api-integration.test.ts +78 -1
  157. package/src/tests/pages-public-html.test.ts +43 -0
  158. package/src/tests/pages-svg-migration.test.ts +42 -0
  159. package/src/tests/preload.ts +4 -1
  160. package/src/tests/profile-sync-warning.test.ts +16 -0
  161. package/src/tests/profile-sync.test.ts +126 -0
  162. package/src/tests/prompt-template-session.test.ts +20 -4
  163. package/src/tests/provider-registration.test.ts +93 -0
  164. package/src/tests/rbac-e2e-helpers.test.ts +53 -0
  165. package/src/tests/rbac-e2e-helpers.ts +19 -2
  166. package/src/tests/routing-affinity-inventory.test.ts +1 -1
  167. package/src/tests/runner-attachments-section.test.ts +22 -2
  168. package/src/tests/runner-context-preamble.test.ts +30 -0
  169. package/src/tests/runner-deployment-resume.test.ts +107 -0
  170. package/src/tests/runner-repo-autostash.test.ts +12 -0
  171. package/src/tests/script-connections.test.ts +4 -0
  172. package/src/tests/seed-scripts.test.ts +2 -2
  173. package/src/tests/session-tokens.test.ts +279 -0
  174. package/src/tests/slack-file-tools-attachments.test.ts +408 -0
  175. package/src/tests/slack-inbound-files.test.ts +847 -0
  176. package/src/tests/star-history-refresh.test.ts +102 -0
  177. package/src/tests/swarm-repos.test.ts +20 -0
  178. package/src/tests/tool-annotations.test.ts +4 -3
  179. package/src/tests/ui-logs-parser.test.ts +176 -0
  180. package/src/tools/create-page.ts +4 -2
  181. package/src/tools/defer-task.ts +239 -0
  182. package/src/tools/repos/update-repo.ts +2 -2
  183. package/src/tools/schedules/create-schedule.ts +1 -0
  184. package/src/tools/schedules/list-schedules.ts +1 -0
  185. package/src/tools/schedules/patch-schedule.ts +1 -0
  186. package/src/tools/schedules/update-schedule.ts +1 -0
  187. package/src/tools/slack-download-file.ts +60 -29
  188. package/src/tools/slack-read.ts +73 -32
  189. package/src/tools/store-progress.ts +16 -146
  190. package/src/tools/tool-config.ts +1 -0
  191. package/src/tools/utils.ts +21 -0
  192. package/src/types.ts +37 -4
  193. package/src/utils/acp-session-token.ts +56 -0
  194. package/src/utils/file-lock.ts +111 -0
  195. package/src/utils/heartbeat-expiry.ts +78 -0
  196. package/src/utils/request-auth-context.ts +2 -0
  197. package/src/utils/task-attachment-links.ts +22 -0
  198. package/templates/skills/code-quality/content.md +8 -5
  199. package/templates/skills/download-task-attachment/content.md +2 -1
  200. package/{plugin/pi-skills → templates/skills}/review-offered-task/SKILL.md +5 -5
  201. package/templates/skills/review-offered-task/config.json +14 -0
  202. package/{plugin/commands/review-offered-task.md → templates/skills/review-offered-task/content.md} +3 -8
  203. package/templates/skills/scheduling/content.md +4 -0
  204. package/templates/skills/slack-interaction/content.md +2 -2
  205. package/templates/skills/work-on-task/SKILL.md +16 -0
  206. package/templates/skills/work-on-task/config.json +14 -0
  207. package/templates/skills/work-on-task/content.md +11 -0
  208. package/dist/assistant-dx3ew4d8.js +0 -212
  209. package/dist/cli-00kyrp1a.js +0 -317
  210. package/dist/cli-n1q8q14d.js +0 -177
  211. package/dist/cli-ss6vybmt.js +0 -269
  212. package/dist/handlers-e9vp2fr8.js +0 -592
  213. package/plugin/commands/work-on-task.md +0 -39
  214. package/plugin/pi-skills/work-on-task/SKILL.md +0 -39
@@ -2,7 +2,9 @@ import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
3
  import { countSessions, getRootTaskChain, getTaskById, listRecentSessions } from "../be/db";
4
4
  import { getTaskSteeringFields } from "../be/steering";
5
+ import { mintSessionToken, revokeSessionToken } from "../be/users";
5
6
  import { AgentTaskSchema, AgentTaskStatusSchema, SteerModeSchema } from "../types";
7
+ import { getRequestAuth } from "../utils/request-auth-context";
6
8
  import { route } from "./route-def";
7
9
  import { jsonError } from "./utils";
8
10
 
@@ -68,6 +70,57 @@ const TaskWithSteeringSchema = AgentTaskSchema.extend({
68
70
 
69
71
  // ─── Route Definitions ───────────────────────────────────────────────────────
70
72
 
73
+ /** Maximum TTL for an ephemeral session token: 7 days. */
74
+ const MAX_SESSION_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
75
+
76
+ const mintSessionTokenRoute = route({
77
+ method: "post",
78
+ path: "/api/sessions/tokens",
79
+ pattern: ["api", "sessions", "tokens"],
80
+ summary: "Mint an ephemeral session token for an ACP provider session",
81
+ description:
82
+ "Returns a short-lived aseph_ bearer for use by the ACP adapter in place of the full " +
83
+ "operator key. The token expires at the requested TTL and is actively revoked when the " +
84
+ "session ends. Only the operator key may mint session tokens.",
85
+ tags: ["Sessions"],
86
+ body: z.object({
87
+ agentId: z.string().min(1),
88
+ taskId: z.string().min(1),
89
+ ttlMs: z.number().int().positive().max(MAX_SESSION_TOKEN_TTL_MS),
90
+ }),
91
+ responses: {
92
+ 200: {
93
+ description: "Minted token plaintext (returned once) and its stable token ID",
94
+ schema: z.object({
95
+ tokenId: z.string(),
96
+ plaintext: z.string(),
97
+ }),
98
+ },
99
+ 400: { description: "Validation error" },
100
+ 401: { description: "Unauthorized" },
101
+ },
102
+ auth: { apiKey: true },
103
+ rbac: { ungated: "operator-only: minting is gated on the full operator key in auth middleware" },
104
+ });
105
+
106
+ const revokeSessionTokenRoute = route({
107
+ method: "delete",
108
+ path: "/api/sessions/tokens/{tokenId}",
109
+ pattern: ["api", "sessions", "tokens", null],
110
+ summary: "Revoke an ephemeral session token",
111
+ description: "Revokes an aseph_ bearer token by ID. No-op if already revoked.",
112
+ tags: ["Sessions"],
113
+ params: z.object({ tokenId: z.string() }),
114
+ responses: {
115
+ 204: { description: "Token revoked (or was already revoked)" },
116
+ 401: { description: "Unauthorized" },
117
+ },
118
+ auth: { apiKey: true },
119
+ rbac: {
120
+ ungated: "operator-only: revocation is gated on the full operator key in auth middleware",
121
+ },
122
+ });
123
+
71
124
  const listSessions = route({
72
125
  method: "get",
73
126
  path: "/api/sessions",
@@ -136,6 +189,35 @@ export async function handleSessions(
136
189
  pathSegments: string[],
137
190
  queryParams: URLSearchParams,
138
191
  ): Promise<boolean> {
192
+ if (mintSessionTokenRoute.match(req.method, pathSegments)) {
193
+ // Restrict to the full operator key — aseph_ and aswt_ bearers must not be
194
+ // able to mint new session tokens.
195
+ if (getRequestAuth(req)?.kind !== "operator") {
196
+ jsonError(res, "Unauthorized", 401);
197
+ return true;
198
+ }
199
+ const parsed = await mintSessionTokenRoute.parse(req, res, pathSegments, queryParams);
200
+ if (!parsed) return true;
201
+ const { agentId, taskId, ttlMs } = parsed.body;
202
+ const token = await mintSessionToken(agentId, taskId, ttlMs);
203
+ mintSessionTokenRoute.respond(res, 200, { tokenId: token.tokenId, plaintext: token.plaintext });
204
+ return true;
205
+ }
206
+
207
+ if (revokeSessionTokenRoute.match(req.method, pathSegments)) {
208
+ // Restrict revocation to the operator key as well.
209
+ if (getRequestAuth(req)?.kind !== "operator") {
210
+ jsonError(res, "Unauthorized", 401);
211
+ return true;
212
+ }
213
+ const parsed = await revokeSessionTokenRoute.parse(req, res, pathSegments, queryParams);
214
+ if (!parsed) return true;
215
+ await revokeSessionToken(parsed.params.tokenId);
216
+ res.writeHead(204);
217
+ res.end();
218
+ return true;
219
+ }
220
+
139
221
  if (listSessions.match(req.method, pathSegments)) {
140
222
  const parsed = await listSessions.parse(req, res, pathSegments, queryParams);
141
223
  if (!parsed) return true;
package/src/http/tasks.ts CHANGED
@@ -515,7 +515,9 @@ const listPausedTasks = route({
515
515
  responses: {
516
516
  200: {
517
517
  description: "Paused task list",
518
- schema: z.object({ tasks: z.array(AgentTaskSchema) }),
518
+ schema: z.object({
519
+ tasks: z.array(AgentTaskSchema.extend({ attachments: z.array(TaskAttachmentSchema) })),
520
+ }),
519
521
  },
520
522
  },
521
523
  });
@@ -1449,7 +1451,13 @@ export async function handleTasks(
1449
1451
  return true;
1450
1452
  }
1451
1453
  const pausedTasks = await getPausedTasksForAgent(myAgentId);
1452
- listPausedTasks.respond(res, 200, { tasks: pausedTasks });
1454
+ const tasks = await Promise.all(
1455
+ pausedTasks.map(async (task) => ({
1456
+ ...task,
1457
+ attachments: await getTaskAttachments(task.id),
1458
+ })),
1459
+ );
1460
+ listPausedTasks.respond(res, 200, { tasks });
1453
1461
  return true;
1454
1462
  }
1455
1463
 
@@ -74,7 +74,8 @@ Choose the path by the shape of the work:
74
74
  Store progress with \`store-progress\` at each milestone. A milestone is a result the lead could act on.
75
75
  The task is done when \`store-progress\` carries status \`completed\` and an \`output\` that names the result and every artifact link. On failure, status \`failed\` and a \`failureReason\` that names what you tried.
76
76
  When the task carries an \`outputSchema\`, \`output\` is JSON that matches it.
77
- When you are blocked after real effort, store the blocker with \`store-progress\` and keep working on what you can. When nothing is left to do, fail the task with a \`failureReason\` that names the blocker. The lead reads both.
77
+ When you are blocked after real effort, store the blocker with \`store-progress\` and keep working on what you can.
78
+ The task has four endings. When you are done: \`completed\`. When the answer needs time (a build, a deploy, a reply): \`defer-task\` with a summary of what you did, when to wake up, and what to check. It completes the task now; the wake-up task continues it. When a person must decide: \`request-human-input\`. When nothing else is possible: \`failed\` with the blocker.
78
79
  `,
79
80
  variables: [],
80
81
  category: "system",
@@ -89,9 +90,10 @@ registerTemplate({
89
90
  Your output is delegation and review. Workers implement, research, analyze, and write. Data gathering, even a quick query, goes to a worker. You answer simple factual questions yourself.
90
91
 
91
92
  \`get-swarm\` is the roster. Route by capability and load.
93
+ Delegate with \`send-task\`. Read a child's result with \`get-task-details\`.
92
94
  A task states the goal, the repo URL when there is one, and the constraints. Workers know git, the skills, and \`store-progress\`.
93
95
  Delegate by the shape of the work: a workflow for multi-step or fan-out work, a schedule for recurring work, a script for bulk data, an inline \`script-run\` for a one-off bulk job you can run yourself. The \`workflow-iterate\`, \`scheduling\`, and \`swarm-scripts\` skills build them.
94
- Research or exploration: tell the worker to use \`/researching\`. A large feature: a \`/planning\` task first, then an \`/implementing\` task with \`parentTaskId\`. A small fix: direct implementation.
96
+ Research or exploration: tell the worker to use the \`researching\` skill. A large feature: a task for the \`planning\` skill first, then a task for the \`implementing\` skill with \`parentTaskId\`. A small fix: direct implementation.
95
97
  A follow-up that continues earlier work carries \`parentTaskId\`. The worker receives the prior context.
96
98
  A task whose result depends on the workers' output: wait for the children with the \`wait-for-task\` script, then merge and complete the task yourself. A turn that ends with children still running leaves the task unfinished.
97
99
 
@@ -12,6 +12,7 @@ import {
12
12
  } from "@agentclientprotocol/sdk";
13
13
  import pkg from "../../package.json";
14
14
  import type { AcpSessionConfigOption } from "../types";
15
+ import { mintAcpSessionToken, revokeAcpSessionToken } from "../utils/acp-session-token";
15
16
  import { fetchInstalledMcpServers } from "../utils/mcp-server-fetcher";
16
17
  import {
17
18
  detachedProcessGroup,
@@ -176,6 +177,10 @@ class ACPSession implements ProviderSession {
176
177
  private output = "";
177
178
  private completionPromise: Promise<ProviderResult>;
178
179
  private completionResolve!: (result: ProviderResult) => void;
180
+ /** ID of the ephemeral aseph_ token minted for this session, or null when
181
+ * the fallback operator key is in use (e.g. during tests or if the API is
182
+ * not yet upgraded). Revoked in finish(). */
183
+ private ephemeralTokenId: string | null = null;
179
184
 
180
185
  constructor(
181
186
  private readonly connection: ClientSideConnection,
@@ -183,8 +188,10 @@ class ACPSession implements ProviderSession {
183
188
  private readonly config: ProviderSessionConfig,
184
189
  sessionId: string,
185
190
  providerMeta?: Record<string, unknown>,
191
+ ephemeralTokenId?: string,
186
192
  ) {
187
193
  this.sessionId = sessionId;
194
+ this.ephemeralTokenId = ephemeralTokenId ?? null;
188
195
  this.completionPromise = new Promise((resolve) => {
189
196
  this.completionResolve = resolve;
190
197
  });
@@ -338,6 +345,11 @@ class ACPSession implements ProviderSession {
338
345
  if (this.completed) return;
339
346
  this.completed = true;
340
347
  this.completionResolve(result);
348
+ // Revoke the ephemeral token now that the session is done. Best-effort:
349
+ // the token expires on its own, so a failure here is not critical.
350
+ if (this.ephemeralTokenId) {
351
+ void revokeAcpSessionToken(this.config.apiUrl, this.config.apiKey, this.ephemeralTokenId);
352
+ }
341
353
  }
342
354
  }
343
355
 
@@ -373,12 +385,23 @@ export class ACPAdapter implements ProviderAdapter {
373
385
  const stream = ndJsonStream(fileSinkWritableStream(proc.stdin), proc.stdout);
374
386
  const connection = new ClientSideConnection(() => client, stream);
375
387
 
388
+ let ephemeralToken: { tokenId: string; plaintext: string } | null = null;
376
389
  try {
377
390
  await connection.initialize({
378
391
  protocolVersion: PROTOCOL_VERSION,
379
392
  clientInfo: { name: "agent-swarm", version: pkg.version },
380
393
  clientCapabilities: { session: { configOptions: { boolean: {} } } },
381
394
  });
395
+ // Mint a short-lived session-scoped bearer so the ACP target receives
396
+ // an aseph_ token rather than the full operator key. Throws on failure
397
+ // so we fail closed instead of falling back to the operator key.
398
+ ephemeralToken = await mintAcpSessionToken(
399
+ config.apiUrl,
400
+ config.apiKey,
401
+ config.agentId,
402
+ config.taskId,
403
+ );
404
+
382
405
  const installedServers = await fetchInstalledMcpServers(
383
406
  config.apiUrl,
384
407
  config.apiKey,
@@ -388,7 +411,7 @@ export class ACPAdapter implements ProviderAdapter {
388
411
  // The v2 draft (@agentclientprotocol/sdk/experimental/v2) adds a fourth
389
412
  // `{ type: "acp", name, serverId }` variant where the client hosts the MCP
390
413
  // server itself and the agent tunnels MCP over the existing ACP connection
391
- // (mcp/connect, mcp/message, mcp/disconnect) instead of a network hop — the
414
+ // (mcp/connect, mcp/message, mcp/disconnect) instead of a network hop -- the
392
415
  // shape for an ACP agent with no network route to the swarm API. Gated on
393
416
  // `mcpCapabilities.acp` and UNSTABLE; not adopted here.
394
417
  const newSession = await connection.newSession({
@@ -399,7 +422,7 @@ export class ACPAdapter implements ProviderAdapter {
399
422
  name: "swarm",
400
423
  url: `${config.apiUrl.replace(/\/+$/, "")}/mcp`,
401
424
  headers: [
402
- { name: "Authorization", value: `Bearer ${config.apiKey}` },
425
+ { name: "Authorization", value: `Bearer ${ephemeralToken.plaintext}` },
403
426
  { name: "X-Agent-ID", value: config.agentId },
404
427
  { name: "X-Source-Task-Id", value: config.taskId },
405
428
  ],
@@ -413,13 +436,26 @@ export class ACPAdapter implements ProviderAdapter {
413
436
  newSession.configOptions ?? [],
414
437
  target.configuredOptions(config),
415
438
  );
416
- session = new ACPSession(connection, proc, config, newSession.sessionId, {
417
- target: target.target,
418
- configOptions: sanitizeAcpConfigOptions(configOptions),
419
- });
439
+ session = new ACPSession(
440
+ connection,
441
+ proc,
442
+ config,
443
+ newSession.sessionId,
444
+ {
445
+ target: target.target,
446
+ configOptions: sanitizeAcpConfigOptions(configOptions),
447
+ },
448
+ ephemeralToken.tokenId,
449
+ );
420
450
  for (const event of preSessionEvents) session.emitFromAcp(event);
421
451
  return session;
422
452
  } catch (err) {
453
+ // Revoke the ephemeral token before re-throwing if ACPSession has not yet
454
+ // taken ownership of it (i.e. setup failed after mint but before the
455
+ // ACPSession constructor ran).
456
+ if (ephemeralToken && !session) {
457
+ void revokeAcpSessionToken(config.apiUrl, config.apiKey, ephemeralToken.tokenId);
458
+ }
423
459
  await terminateProcessGroup(proc.pid);
424
460
  throw new Error(`ACP target failed during startup: ${scrubSecrets(formatError(err))}`);
425
461
  }
@@ -191,19 +191,36 @@ export async function checkPiMonoCredentials(
191
191
  env: Record<string, string | undefined>,
192
192
  opts: CredCheckOptions = {},
193
193
  ): Promise<CredStatus> {
194
- // Determine Bedrock SDK mode:
195
- // - Explicit: BEDROCK_AUTH_MODE=sdk
194
+ // Determine the Bedrock mode:
195
+ // - Explicit: BEDROCK_AUTH_MODE=sdk — the AWS SDK default credential chain
196
+ // - Explicit: BEDROCK_AUTH_MODE=bearer — an explicit Bedrock API key in
197
+ // AWS_BEARER_TOKEN_BEDROCK, which the AWS SDK picks up as the
198
+ // bearer identity for the Bedrock clients
196
199
  // - Fallback: BEDROCK_AUTH_MODE absent AND MODEL_OVERRIDE starts with
197
200
  // "amazon-bedrock/" (preserves today's prefix-inference semantics)
198
- // BEDROCK_AUTH_MODE=bearer is declared/validated but the full bearer-token
199
- // path is not implemented yet — it falls through to the standard auth check.
201
+ // Both explicit modes share the region check and the enumeration probe; they
202
+ // differ in what has to be present up front and in how readiness is reported.
200
203
  const bedrockAuthMode = env.BEDROCK_AUTH_MODE?.toLowerCase();
204
+ const isBedrockBearer = bedrockAuthMode === "bearer";
201
205
  const isBedrockSdk =
202
206
  bedrockAuthMode === "sdk" ||
203
207
  (bedrockAuthMode === undefined &&
204
208
  env.MODEL_OVERRIDE?.toLowerCase().startsWith("amazon-bedrock/"));
205
209
 
206
- if (isBedrockSdk) {
210
+ if (isBedrockBearer && !env.AWS_BEARER_TOKEN_BEDROCK) {
211
+ // The token is the whole point of this mode; without it there is nothing
212
+ // the probe could authenticate with, and the standard API keys do not
213
+ // apply to Bedrock, so do not fall through to them.
214
+ return {
215
+ ready: false,
216
+ missing: ["AWS_BEARER_TOKEN_BEDROCK"],
217
+ hint: "BEDROCK_AUTH_MODE=bearer requires AWS_BEARER_TOKEN_BEDROCK (a Bedrock API key); set it, or use BEDROCK_AUTH_MODE=sdk to authenticate through the AWS credential chain.",
218
+ bedrockModels: [],
219
+ bedrockRegion: env.AWS_REGION ?? "",
220
+ };
221
+ }
222
+
223
+ if (isBedrockSdk || isBedrockBearer) {
207
224
  const region = env.AWS_REGION;
208
225
  if (!region) {
209
226
  // Do NOT fabricate a region. A guessed `us-east-1` can differ from where
@@ -231,7 +248,7 @@ export async function checkPiMonoCredentials(
231
248
  return {
232
249
  ready: true,
233
250
  missing: [],
234
- satisfiedBy: "sdk-delegated",
251
+ satisfiedBy: isBedrockBearer ? "env" : "sdk-delegated",
235
252
  hint: `Bedrock models invocable in ${region} enumerated (${bedrockModels.length} usable; ListFoundationModels + ListInferenceProfiles).`,
236
253
  bedrockModels,
237
254
  bedrockRegion: region,
@@ -1,7 +1,7 @@
1
1
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
2
  import { unlinkSync } from "node:fs";
3
- import { closeDb, createScheduledTask, initDb } from "../be/db";
4
- import { calculateNextRun } from "./scheduler";
3
+ import { closeDb, createAgent, createScheduledTask, createTaskExtended, initDb } from "../be/db";
4
+ import { calculateNextRun, createStandaloneScheduleTask } from "./scheduler";
5
5
 
6
6
  const TEST_DB_PATH = "./test-scheduler.sqlite";
7
7
 
@@ -151,3 +151,50 @@ describe("calculateNextRun", () => {
151
151
  expect(nextRun).toBe("2026-01-19T09:00:00.000Z");
152
152
  });
153
153
  });
154
+
155
+ describe("createStandaloneScheduleTask", () => {
156
+ test("passes the schedule's parentTaskId through to the created task", async () => {
157
+ const agent = await createAgent({
158
+ name: "Deferred Wake-up Worker",
159
+ description: "Agent that owns the deferred task",
160
+ role: "worker",
161
+ isLead: false,
162
+ status: "idle",
163
+ maxTasks: 1,
164
+ capabilities: [],
165
+ });
166
+ const parent = await createTaskExtended("original work that needed time", {
167
+ agentId: agent.id,
168
+ source: "mcp",
169
+ });
170
+
171
+ const schedule = await createScheduledTask({
172
+ name: "test-deferred-parent",
173
+ taskTemplate: `Resume task ${parent.id}: the deploy should be green now`,
174
+ scheduleType: "one_time",
175
+ nextRunAt: new Date(Date.now() + 60_000).toISOString(),
176
+ targetAgentId: agent.id,
177
+ createdByAgentId: agent.id,
178
+ taskType: "deferred",
179
+ tags: ["deferred"],
180
+ parentTaskId: parent.id,
181
+ });
182
+
183
+ const task = await createStandaloneScheduleTask(schedule);
184
+ expect(task.parentTaskId).toBe(parent.id);
185
+ expect(task.taskType).toBe("deferred");
186
+ expect(task.tags).toContain("deferred");
187
+ });
188
+
189
+ test("a schedule without parentTaskId creates a parentless task", async () => {
190
+ const schedule = await createScheduledTask({
191
+ name: "test-no-parent",
192
+ taskTemplate: "Ordinary scheduled work",
193
+ cronExpression: "0 9 * * *",
194
+ timezone: "UTC",
195
+ });
196
+
197
+ const task = await createStandaloneScheduleTask(schedule);
198
+ expect(task.parentTaskId).toBeFalsy();
199
+ });
200
+ });
@@ -73,6 +73,10 @@ export async function createStandaloneScheduleTask(
73
73
  source: "schedule",
74
74
  requestedByUserId: schedule.createdBy,
75
75
  contextKey: scheduleContextKey({ scheduleId: schedule.id }),
76
+ // Set only by `defer-task`. An explicit parent wins over the sibling-awareness
77
+ // auto-wiring (see withSiblingAwareness in src/tasks/sibling-awareness.ts), so
78
+ // the wake-up run continues the deferred task rather than a random sibling.
79
+ parentTaskId: schedule.parentTaskId,
76
80
  });
77
81
  }
78
82
 
@@ -12,6 +12,7 @@ export const SDK_TOOL_NAME_MAP = {
12
12
  task_list: "get-tasks",
13
13
  task_get: "get-task-details",
14
14
  task_storeProgress: "store-progress",
15
+ task_defer: "defer-task",
15
16
  task_poll: "poll-task",
16
17
  task_send: "send-task",
17
18
  task_cancel: "cancel-task", // destructive
@@ -143,6 +143,14 @@ declare module "swarm-sdk" {
143
143
  task_list(args?: Record<string, unknown>): Promise<unknown>;
144
144
  task_get(args: { taskId: string }): Promise<unknown>;
145
145
  task_storeProgress(args: Record<string, unknown>): Promise<unknown>;
146
+ task_defer(args: {
147
+ taskId: string;
148
+ delayMs?: number;
149
+ runAt?: string;
150
+ summary: string;
151
+ note: string;
152
+ checks?: string[];
153
+ }): Promise<unknown>;
146
154
  task_poll(args?: Record<string, unknown>): Promise<unknown>;
147
155
  // --- kv ---
148
156
  kv_get<T = unknown>(args: {
@@ -125,6 +125,14 @@ declare module "swarm-sdk" {
125
125
  task_list(args?: Record<string, unknown>): Promise<unknown>;
126
126
  task_get(args: { taskId: string }): Promise<unknown>;
127
127
  task_storeProgress(args: Record<string, unknown>): Promise<unknown>;
128
+ task_defer(args: {
129
+ taskId: string;
130
+ delayMs?: number;
131
+ runAt?: string;
132
+ summary: string;
133
+ note: string;
134
+ checks?: string[];
135
+ }): Promise<unknown>;
128
136
  task_poll(args?: Record<string, unknown>): Promise<unknown>;
129
137
  // --- kv ---
130
138
  kv_get<T = unknown>(args: {
package/src/server.ts CHANGED
@@ -26,6 +26,7 @@ import { registerCreateMetricTool } from "./tools/create-metric";
26
26
  import { registerCreatePageTool } from "./tools/create-page";
27
27
  import { registerCredentialBindingsTool } from "./tools/credential-bindings";
28
28
  import { registerDbQueryTool } from "./tools/db-query";
29
+ import { registerDeferTaskTool } from "./tools/defer-task";
29
30
  import { registerDeleteChannelTool } from "./tools/delete-channel";
30
31
  import { registerDeletePageTool } from "./tools/delete-page";
31
32
  import { registerGetMetricsTool } from "./tools/get-metrics";
@@ -421,6 +422,7 @@ export async function createServer(opts: { scriptsOnly?: boolean; fullSurface?:
421
422
  if (hasCapability("scheduling")) {
422
423
  registerListSchedulesTool(server);
423
424
  registerCreateScheduleTool(server);
425
+ registerDeferTaskTool(server);
424
426
  registerUpdateScheduleTool(server);
425
427
  registerPatchScheduleTool(server);
426
428
  registerDeleteScheduleTool(server);
@@ -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", () => {