@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
@@ -89,6 +89,7 @@ const scheduleDataShape = {
89
89
  lastRunAt: z.string().optional(),
90
90
  nextRunAt: z.string().optional(),
91
91
  createdByAgentId: z.string().optional(),
92
+ parentTaskId: z.string().optional(),
92
93
  timezone: z.string().optional(),
93
94
  model: z.string().optional(),
94
95
  modelTier: ModelTierSchema.optional(),
@@ -3,16 +3,23 @@ import * as z from "zod";
3
3
  import { getAgentById } from "@/be/db";
4
4
  import { getSlackApp } from "@/slack/app";
5
5
  import { DEFAULT_DOWNLOAD_DIR, downloadFile, getFileInfo } from "@/slack/files";
6
+ import { attachableTask, attachSlackFilesForAgent } from "@/slack/inbound-files";
6
7
  import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
7
8
 
9
+ /** `F0123ABCD` out of a `files.slack.com/files-pri/<team>-<file>/…` URL. */
10
+ function fileIdFromUrl(url: string): string | undefined {
11
+ return /\/files-pri\/[A-Z0-9]+-([A-Z0-9]+)\//.exec(url)?.[1];
12
+ }
13
+
8
14
  export const registerSlackDownloadFileTool = (server: McpServer) => {
9
15
  createToolRegistrar(server)(
10
16
  "slack-download-file",
11
17
  {
12
18
  title: "Download file from Slack",
13
19
  description:
14
- "Download a file from Slack by file ID or URL. Files are saved to the agent's download directory on the shared disk by default.",
15
- annotations: { readOnlyHint: true, openWorldHint: true },
20
+ "Download a file from Slack by file ID or URL. From a task, the file is stored as an attachment of that task and the result carries a ready-to-run `fetchCommand` to get the bytes into your container. Without a task, the file is saved on the API server's disk, which your container usually can't read.",
21
+ // Not read-only: from a task it uploads the file and records an attachment.
22
+ annotations: { readOnlyHint: false, openWorldHint: true },
16
23
 
17
24
  inputSchema: z.object({
18
25
  fileId: z
@@ -24,11 +31,17 @@ export const registerSlackDownloadFileTool = (server: McpServer) => {
24
31
  .url()
25
32
  .optional()
26
33
  .describe("Direct URL to download (url_private_download from a file object)."),
34
+ taskId: z
35
+ .uuid()
36
+ .optional()
37
+ .describe(
38
+ "Task to attach the file to. Defaults to the task you are working on; must be a task you own or created.",
39
+ ),
27
40
  savePath: z
28
41
  .string()
29
42
  .optional()
30
43
  .describe(
31
- "Where to save the file. Can be a directory or full path. Defaults to /workspace/shared/downloads/{agentId}/slack/",
44
+ "Only without a task: where to save the file on the API server (directory or full path). Defaults to /workspace/shared/downloads/{agentId}/slack/.",
32
45
  ),
33
46
  filename: z
34
47
  .string()
@@ -36,7 +49,15 @@ export const registerSlackDownloadFileTool = (server: McpServer) => {
36
49
  .describe("Filename to use when saving. Only used if savePath is a directory."),
37
50
  }),
38
51
  outputSchema: swarmToolOutputSchema({
39
- savedPath: z.string().optional(),
52
+ taskId: z.string().optional(),
53
+ attachmentId: z.string().optional(),
54
+ fetchCommand: z.string().optional(),
55
+ savedPath: z
56
+ .string()
57
+ .optional()
58
+ .describe(
59
+ "Only without a task: the path on the API server's disk, not in your container.",
60
+ ),
40
61
  fileInfo: z
41
62
  .looseObject({
42
63
  id: z.string().optional(),
@@ -47,7 +68,7 @@ export const registerSlackDownloadFileTool = (server: McpServer) => {
47
68
  .optional(),
48
69
  }),
49
70
  },
50
- async ({ fileId, url, savePath, filename }, requestInfo, _meta) => {
71
+ async ({ fileId, url, taskId, savePath, filename }, requestInfo, _meta) => {
51
72
  if (!requestInfo.agentId) {
52
73
  return toolErr("Agent ID not found.");
53
74
  }
@@ -72,33 +93,42 @@ export const registerSlackDownloadFileTool = (server: McpServer) => {
72
93
  return toolErr("Slack bot token not configured.");
73
94
  }
74
95
 
96
+ const task = await attachableTask(agent.id, taskId ?? requestInfo.sourceTaskId);
97
+ if (taskId && !task) {
98
+ return toolErr("You don't have context for this task.");
99
+ }
100
+
75
101
  try {
76
- let downloadUrl = url;
77
- let fileInfo:
78
- | {
79
- id: string;
80
- name: string;
81
- mimetype: string;
82
- size: number;
83
- }
84
- | undefined;
85
-
86
- // If fileId provided, get file info first
87
- if (fileId) {
88
- const info = await getFileInfo(app.client, fileId);
102
+ const id = fileId ?? (url ? fileIdFromUrl(url) : undefined);
103
+ const info = id ? await getFileInfo(app.client, id) : null;
104
+ if (fileId && !info) {
105
+ return toolErr(`File not found: ${fileId}`);
106
+ }
107
+ const fileInfo = info
108
+ ? { id: info.id, name: info.name, mimetype: info.mimetype, size: info.size }
109
+ : undefined;
110
+
111
+ if (task) {
89
112
  if (!info) {
90
- return toolErr(`File not found: ${fileId}`);
113
+ return toolErr("Could not identify the Slack file in that URL; pass fileId instead.");
91
114
  }
92
-
93
- downloadUrl = info.url_private_download;
94
- fileInfo = {
95
- id: info.id,
96
- name: info.name,
97
- mimetype: info.mimetype,
98
- size: info.size,
99
- };
115
+ const outcome = (await attachSlackFilesForAgent(app.client, task, [info], agent.id)).get(
116
+ info.id,
117
+ );
118
+ if (!outcome || "reason" in outcome) {
119
+ return toolErr(`Failed to attach file: ${outcome?.reason ?? "unknown error"}`);
120
+ }
121
+ const { attachment, fetchCommand } = outcome;
122
+ return toolOk(
123
+ `Attached ${attachment.name} to task ${task.id} (attachment ${attachment.id}).`,
124
+ {
125
+ details: `Get it into your container with:\n${fetchCommand}`,
126
+ data: { taskId: task.id, attachmentId: attachment.id, fetchCommand, fileInfo },
127
+ },
128
+ );
100
129
  }
101
130
 
131
+ const downloadUrl = info?.url_private_download ?? url;
102
132
  if (!downloadUrl) {
103
133
  return toolErr("No download URL available.");
104
134
  }
@@ -125,8 +155,9 @@ export const registerSlackDownloadFileTool = (server: McpServer) => {
125
155
  return toolErr(`Failed to download file: ${result.error}`);
126
156
  }
127
157
 
128
- const successMsg = `File downloaded successfully to ${result.savedPath}`;
129
- return toolOk(successMsg, { data: { savedPath: result.savedPath, fileInfo } });
158
+ return toolOk(`File saved on the API server at ${result.savedPath}.`, {
159
+ data: { savedPath: result.savedPath, fileInfo },
160
+ });
130
161
  } catch (error) {
131
162
  const errorMsg = error instanceof Error ? error.message : String(error);
132
163
  return toolErr(`Failed to download file: ${errorMsg}`);
@@ -4,13 +4,16 @@ import { getAgentById, getInboxMessageById, getTaskById } from "@/be/db";
4
4
  import { can } from "@/rbac";
5
5
  import { getSlackApp } from "@/slack/app";
6
6
  import { withAutoJoin } from "@/slack/channel-join";
7
- import { downloadFile } from "@/slack/files";
7
+ import { downloadFile, type SlackFile } from "@/slack/files";
8
+ import { attachableTask, attachSlackFilesForAgent } from "@/slack/inbound-files";
8
9
  import { extractSlackMessageText } from "@/slack/message-text";
9
10
  import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
11
+ import type { AgentTask } from "@/types";
10
12
 
11
13
  /**
12
- * Default download directory for auto-downloaded Slack files (inside MCP container).
13
- * This differs from the agent's /workspace/shared path as the MCP server runs in a separate container.
14
+ * Where files are auto-downloaded when there is no task to attach them to.
15
+ * This is the API server's disk: a worker container only sees it if the
16
+ * deployment mounts the shared volume there.
14
17
  */
15
18
  const AUTO_DOWNLOAD_DIR = "/app/shared/downloads/slack";
16
19
 
@@ -21,7 +24,13 @@ const SlackFileSchema = z.looseObject({
21
24
  filetype: z.string().optional(),
22
25
  size: z.number().optional(),
23
26
  url_private_download: z.string().optional(),
24
- localPath: z.string().optional(),
27
+ attachmentId: z.string().optional(),
28
+ fetchCommand: z.string().optional(),
29
+ notAttached: z.string().optional(),
30
+ localPath: z
31
+ .string()
32
+ .optional()
33
+ .describe("Only without a task: the path on the API server's disk, not in your container."),
25
34
  });
26
35
 
27
36
  const SlackMessageSchema = z.looseObject({
@@ -39,8 +48,9 @@ export const registerSlackReadTool = (server: McpServer) => {
39
48
  {
40
49
  title: "Read Slack thread/channel history",
41
50
  description:
42
- "Read messages from a Slack thread or channel. Use inboxMessageId or taskId to read from a thread you have context for, or provide channelId directly for channel history (leads only).",
43
- annotations: { readOnlyHint: true, openWorldHint: true },
51
+ "Read messages from a Slack thread or channel. Use inboxMessageId or taskId to read from a thread you have context for, or provide channelId directly for channel history (leads only). From a task, files in the messages are stored as attachments of that task, each with a ready-to-run `fetchCommand`.",
52
+ // Not read-only: from a task it uploads the thread's files as attachments.
53
+ annotations: { readOnlyHint: false, openWorldHint: true },
44
54
 
45
55
  inputSchema: z.object({
46
56
  inboxMessageId: z.uuid().optional().describe("Read thread history for an inbox message."),
@@ -63,7 +73,9 @@ export const registerSlackReadTool = (server: McpServer) => {
63
73
  includeFiles: z
64
74
  .boolean()
65
75
  .default(true)
66
- .describe("Include file attachments in the response (default: true)."),
76
+ .describe(
77
+ "Include file attachments in the response (default: true). From a task, they are also stored as attachments of that task.",
78
+ ),
67
79
  }),
68
80
  outputSchema: swarmToolOutputSchema({
69
81
  channelId: z.string().optional(),
@@ -87,6 +99,7 @@ export const registerSlackReadTool = (server: McpServer) => {
87
99
 
88
100
  let slackChannelId: string | undefined = channelId;
89
101
  let slackThreadTs: string | undefined = threadTs;
102
+ let contextTask: AgentTask | null = null;
90
103
 
91
104
  // Determine Slack context from inbox message or task
92
105
  if (inboxMessageId) {
@@ -110,6 +123,7 @@ export const registerSlackReadTool = (server: McpServer) => {
110
123
  }
111
124
  slackChannelId = task.slackChannelId;
112
125
  slackThreadTs = task.slackThreadTs;
126
+ contextTask = task;
113
127
  } else if (channelId) {
114
128
  // Direct channel access requires lead privileges
115
129
  const decision = can({
@@ -209,6 +223,27 @@ export const registerSlackReadTool = (server: McpServer) => {
209
223
  // Get token for auto-download
210
224
  const token = process.env.SLACK_BOT_TOKEN;
211
225
 
226
+ // Files go to the calling task's attachments; without one, to the API disk.
227
+ const attachTo =
228
+ contextTask ?? (await attachableTask(agent.id, taskId ?? requestInfo.sourceTaskId));
229
+ if (taskId && !attachTo) {
230
+ return toolErr("You don't have context for this task.", { data: { messages: [] } });
231
+ }
232
+
233
+ type FileInfo = {
234
+ id: string;
235
+ name: string;
236
+ mimetype: string;
237
+ filetype: string;
238
+ size: number;
239
+ url_private_download: string;
240
+ attachmentId?: string;
241
+ fetchCommand?: string;
242
+ notAttached?: string;
243
+ localPath?: string;
244
+ };
245
+ const toAttach: Array<{ raw: RawFile; info: FileInfo }> = [];
246
+
212
247
  // Format messages
213
248
  const messages: Array<{
214
249
  user: string | undefined;
@@ -216,15 +251,7 @@ export const registerSlackReadTool = (server: McpServer) => {
216
251
  isBot: boolean;
217
252
  text: string;
218
253
  ts: string;
219
- files?: Array<{
220
- id: string;
221
- name: string;
222
- mimetype: string;
223
- filetype: string;
224
- size: number;
225
- url_private_download: string;
226
- localPath?: string;
227
- }>;
254
+ files?: FileInfo[];
228
255
  }> = [];
229
256
 
230
257
  for (const m of rawMessages) {
@@ -243,22 +270,12 @@ export const registerSlackReadTool = (server: McpServer) => {
243
270
  }
244
271
 
245
272
  // Extract file information if includeFiles is true
246
- let files:
247
- | Array<{
248
- id: string;
249
- name: string;
250
- mimetype: string;
251
- filetype: string;
252
- size: number;
253
- url_private_download: string;
254
- localPath?: string;
255
- }>
256
- | undefined;
273
+ let files: FileInfo[] | undefined;
257
274
 
258
275
  if (includeFiles && m.files && m.files.length > 0) {
259
276
  files = [];
260
277
  for (const f of m.files) {
261
- const fileInfo: (typeof files)[number] = {
278
+ const fileInfo: FileInfo = {
262
279
  id: f.id,
263
280
  name: f.name,
264
281
  mimetype: f.mimetype,
@@ -267,8 +284,10 @@ export const registerSlackReadTool = (server: McpServer) => {
267
284
  url_private_download: f.url_private_download,
268
285
  };
269
286
 
270
- // Auto-download file if token is available
271
- if (token && f.url_private_download) {
287
+ if (attachTo) {
288
+ toAttach.push({ raw: f, info: fileInfo });
289
+ } else if (token && f.url_private_download) {
290
+ // Auto-download file if token is available
272
291
  const savePath = `${AUTO_DOWNLOAD_DIR}/${f.id}_${f.name}`;
273
292
  try {
274
293
  const downloadResult = await downloadFile({
@@ -298,6 +317,24 @@ export const registerSlackReadTool = (server: McpServer) => {
298
317
  });
299
318
  }
300
319
 
320
+ if (attachTo && toAttach.length > 0) {
321
+ const outcomes = await attachSlackFilesForAgent(
322
+ client,
323
+ attachTo,
324
+ toAttach.map(({ raw }) => raw as SlackFile),
325
+ agent.id,
326
+ );
327
+ for (const { info } of toAttach) {
328
+ const outcome = outcomes.get(info.id);
329
+ if (outcome && "attachment" in outcome) {
330
+ info.attachmentId = outcome.attachment.id;
331
+ info.fetchCommand = outcome.fetchCommand;
332
+ } else {
333
+ info.notAttached = outcome?.reason ?? "not downloaded";
334
+ }
335
+ }
336
+ }
337
+
301
338
  // Format for text output
302
339
  const textOutput = messages
303
340
  .map((m) => {
@@ -306,8 +343,12 @@ export const registerSlackReadTool = (server: McpServer) => {
306
343
  const fileList = m.files
307
344
  .map((f) => {
308
345
  let line = ` - ${f.name} (${f.mimetype}, ${Math.round(f.size / 1024)} KB)`;
309
- if (f.localPath) {
310
- line += ` [Downloaded: ${f.localPath}]`;
346
+ if (f.fetchCommand) {
347
+ line += ` [Task attachment ${f.attachmentId}: ${f.fetchCommand}]`;
348
+ } else if (f.notAttached) {
349
+ line += ` [Not attached: ${f.notAttached}]`;
350
+ } else if (f.localPath) {
351
+ line += ` [Downloaded on the API server: ${f.localPath}]`;
311
352
  }
312
353
  return line;
313
354
  })
@@ -7,25 +7,20 @@ import {
7
7
  getAgentById,
8
8
  getDbClient,
9
9
  getResolvedConfig,
10
- getSessionLogsByTaskId,
11
10
  getTaskById,
12
11
  insertTaskAttachment,
13
12
  updateAgentStatusFromCapacity,
14
13
  updateTaskProgress,
15
14
  } from "@/be/db";
16
- import { getEmbeddingProvider, getMemoryStore } from "@/be/memory";
17
- import { getRetrievalsForTask } from "@/be/memory/raters/retrieval";
18
- import { runServerRaters } from "@/be/memory/raters/run-server-raters";
19
15
  import { AgentFsProvider } from "@/fs/agent-fs-provider";
20
- import { shouldPersistTaskCompletionMemory } from "@/memory/automatic-task-gate";
16
+ import { runTaskTerminalEffects } from "@/tasks/task-terminal-effects";
21
17
  import {
22
18
  getTaskOutputValidationError,
23
19
  guardTerminalTaskResultWrite,
24
20
  } from "@/tasks/terminal-result-guard";
25
- import { createWorkerTaskFollowUp } from "@/tasks/worker-follow-up";
26
21
  import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
27
22
  import { AgentTaskStatusSchema, AttachmentInputSchema, isTerminalTaskStatus } from "@/types";
28
- import { scrubSecrets } from "../utils/secret-scrubber";
23
+ import { scrubSecrets } from "@/utils/secret-scrubber";
29
24
 
30
25
  // Phase 11: the `cost` / `costData` field was removed from this tool's input
31
26
  // schema. Adapters (claude/codex/pi/opencode/devin/claude-managed) are the
@@ -406,145 +401,20 @@ export const registerStoreProgressTool = (server: McpServer) => {
406
401
  !("wasNoOp" in result && result.wasNoOp) &&
407
402
  !("wasForcedOverwrite" in result && result.wasForcedOverwrite);
408
403
 
409
- // Index completed and failed tasks as memory (async, non-blocking).
410
- // Skip on no-op (idempotent re-call on terminal task) to avoid duplicate
411
- // memory entries / vector index pollution.
412
- // Automatic/recurring tasks are noisy by default; require explicit opt-in.
413
- if (
414
- shouldRunTerminalSideEffects &&
415
- shouldPersistTaskCompletionMemory(result.task, persistMemory)
416
- ) {
417
- (async () => {
418
- try {
419
- const taskContent =
420
- status === "completed"
421
- ? `Task: ${result.task!.task}\n\nOutput:\n${output || "(no output)"}`
422
- : `Task: ${result.task!.task}\n\nFailure reason:\n${failureReason || "No reason provided"}\n\nThis task failed. Learn from this to avoid repeating the mistake.`;
423
-
424
- // Skip indexing if there's truly no content
425
- if (taskContent.length < 30) return;
426
-
427
- const store = getMemoryStore();
428
- const provider = getEmbeddingProvider();
429
-
430
- const memory = await store.store({
431
- agentId: requestInfo.agentId ?? null,
432
- content: taskContent,
433
- name: `Task: ${result.task!.task.slice(0, 80)}`,
434
- scope: "agent",
435
- source: "task_completion",
436
- sourceTaskId: taskId,
437
- });
438
- const embedding = await provider.embed(taskContent);
439
- if (embedding) {
440
- await store.updateEmbedding(memory.id, embedding, provider.name);
441
- }
442
-
443
- // Auto-promote high-value completions to swarm memory (P3)
444
- const shouldShareWithSwarm =
445
- status === "completed" &&
446
- (result.task!.taskType === "research" ||
447
- result.task!.tags?.includes("knowledge") ||
448
- result.task!.tags?.includes("shared"));
449
-
450
- if (shouldShareWithSwarm) {
451
- try {
452
- const swarmMemory = await store.store({
453
- agentId: requestInfo.agentId ?? null,
454
- scope: "swarm",
455
- name: `Shared: ${result.task!.task.slice(0, 80)}`,
456
- content: `Task completed by agent ${requestInfo.agentId}:\n\n${taskContent}`,
457
- source: "task_completion",
458
- sourceTaskId: taskId,
459
- });
460
- const swarmEmbedding = await provider.embed(taskContent);
461
- if (swarmEmbedding) {
462
- await store.updateEmbedding(swarmMemory.id, swarmEmbedding, provider.name);
463
- }
464
- } catch {
465
- // Non-blocking — swarm memory promotion failure is not critical
466
- }
467
- }
468
- } catch {
469
- // Non-blocking — task completion memory failure should not affect task status
470
- }
471
- })().catch((err) =>
472
- console.error(
473
- "[store-progress] task completion memory write failed:",
474
- scrubSecrets(err instanceof Error ? err.message : String(err)),
475
- ),
476
- );
477
- }
478
-
479
- if (shouldRunTerminalSideEffects) {
480
- // Memory rater v1.5 — fire server-side raters on task completion.
481
- // Plan: thoughts/taras/plans/2026-05-05-memory-rater-v1.5/step-2.md §5
482
- //
483
- // Read `memory_retrieval` rows for this task + concatenated session_logs
484
- // and hand both to `runServerRaters`, which iterates the allow-listed
485
- // server raters (currently just `implicit-citation`), stamps source,
486
- // applies the configured weight multiplier, and persists via
487
- // `applyRating`. The orchestration is extracted so it can be unit-tested
488
- // with stub raters (see `src/tests/run-server-raters.test.ts`).
489
- //
490
- // Fire-and-forget: rater failure must NEVER affect task status.
491
- (async () => {
492
- try {
493
- const retrievals = await getRetrievalsForTask(taskId);
494
- if (retrievals.length === 0) return;
495
-
496
- const retrievedMemoryIds = retrievals.map((r) => r.memoryId);
497
- const logs = await getSessionLogsByTaskId(taskId);
498
- const evidence = logs.map((l) => l.content).join("\n");
499
-
500
- await runServerRaters({
501
- taskId,
502
- agentId: requestInfo.agentId ?? "",
503
- retrievedMemoryIds,
504
- evidence,
505
- });
506
- } catch (err) {
507
- console.error(
508
- "[store-progress] server-rater fire failed:",
509
- err instanceof Error ? err.message : String(err),
510
- );
511
- }
512
- })().catch((err) =>
513
- console.error(
514
- "[store-progress] server rater run failed:",
515
- scrubSecrets(err instanceof Error ? err.message : String(err)),
516
- ),
517
- );
518
- }
519
-
520
- // Create follow-up task for the lead when a worker task finishes.
521
- // This replaces the old poll-based tasks_finished trigger which was unreliable.
522
- // Skip for workflow-managed tasks — the workflow engine handles sequencing via resume.ts.
523
- // Skip on no-op (idempotent re-call on terminal task) to avoid duplicate follow-ups.
524
- if (
525
- status &&
526
- result.success &&
527
- result.task &&
528
- !result.task.workflowRunId &&
529
- !("wasNoOp" in result && result.wasNoOp) &&
530
- !("wasForcedOverwrite" in result && result.wasForcedOverwrite)
531
- ) {
532
- try {
533
- const followUp = await createWorkerTaskFollowUp({
534
- task: result.task,
535
- status,
536
- output,
537
- failureReason,
538
- });
539
- if (followUp) {
540
- console.log(
541
- `[store-progress] Created follow-up task ${followUp.id.slice(0, 8)} for ${status} task ${taskId.slice(0, 8)}`,
542
- );
543
- }
544
- } catch (err) {
545
- // Non-blocking — follow-up task creation failure should not affect the store-progress response
546
- console.warn(`[store-progress] Failed to create follow-up task: ${err}`);
547
- }
404
+ // Post-commit terminal side effects (completion memory, server raters,
405
+ // lead follow-up). Shared with `defer-task` — see
406
+ // src/tasks/task-terminal-effects.ts. Skipped on no-op (idempotent
407
+ // re-call on a terminal task) and on forced text-only overwrites, so a
408
+ // replay never duplicates memories or follow-up tasks.
409
+ if (shouldRunTerminalSideEffects && status) {
410
+ await runTaskTerminalEffects({
411
+ task: result.task!,
412
+ status,
413
+ output,
414
+ failureReason,
415
+ agentId: requestInfo.agentId,
416
+ persistMemory,
417
+ });
548
418
  }
549
419
 
550
420
  const { success, message } = result;
@@ -22,6 +22,7 @@ export const CORE_TOOLS = new Set([
22
22
  "task-action", // claim/release/accept/reject
23
23
  "send-task", // delegate subtasks
24
24
  "get-tasks", // list/filter tasks
25
+ "defer-task", // complete now, wake up later to continue
25
26
 
26
27
  // Memory (used at session start)
27
28
  "memory-search", // recall relevant context
@@ -229,6 +229,23 @@ const scriptRunNudge = (r: SwarmToolResult): string | undefined => {
229
229
  return !r.ok && body?.error === "timeout" ? SCRIPT_RUN_TIMEOUT_NUDGE : scriptAuthoringNudge(r);
230
230
  };
231
231
 
232
+ const SLACK_API_DISK_NUDGE =
233
+ "That path is on the API server's disk, not in your container — call this from a task you own (or pass its taskId) to get the file as a task attachment with a fetchCommand.";
234
+
235
+ const slackDownloadFileNudge = (r: SwarmToolResult): string | undefined =>
236
+ r.ok && (r.data as { savedPath?: unknown } | undefined)?.savedPath
237
+ ? SLACK_API_DISK_NUDGE
238
+ : undefined;
239
+
240
+ const slackReadNudge = (r: SwarmToolResult): string | undefined => {
241
+ if (!r.ok) return undefined;
242
+ const messages = (r.data as { messages?: Array<{ files?: Array<{ localPath?: unknown }> }> })
243
+ ?.messages;
244
+ return messages?.some((m) => m.files?.some((f) => Boolean(f.localPath)))
245
+ ? SLACK_API_DISK_NUDGE
246
+ : undefined;
247
+ };
248
+
232
249
  const workflowLongScriptTimeoutNudge = (r: SwarmToolResult): string | undefined => {
233
250
  if (!r.ok) return undefined;
234
251
  const hint = (r.data as { longScriptTimeoutHint?: unknown } | undefined)?.longScriptTimeoutHint;
@@ -241,6 +258,8 @@ const workflowLongScriptTimeoutNudge = (r: SwarmToolResult): string | undefined
241
258
  * single sentence; derive only from already-scrubbed result fields.
242
259
  */
243
260
  export const NUDGES: Record<string, (result: SwarmToolResult) => string | undefined> = {
261
+ "defer-task": (r) =>
262
+ r.ok ? "Stop working on this task now; the wake-up task will carry your note." : undefined,
244
263
  "script-run": scriptRunNudge,
245
264
  "script-upsert": scriptAuthoringNudge,
246
265
  "launch-script-run": scriptAuthoringNudge,
@@ -249,6 +268,8 @@ export const NUDGES: Record<string, (result: SwarmToolResult) => string | undefi
249
268
  "update-workflow": workflowLongScriptTimeoutNudge,
250
269
  "patch-workflow": workflowLongScriptTimeoutNudge,
251
270
  "patch-workflow-node": workflowLongScriptTimeoutNudge,
271
+ "slack-download-file": slackDownloadFileNudge,
272
+ "slack-read": slackReadNudge,
252
273
  "script-search": (r) => {
253
274
  if (!r.ok) return undefined;
254
275
  // proxyScriptsApi wraps the parsed HTTP body as data = { status, data },