@desplega.ai/agent-swarm 1.147.0 → 1.149.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 (185) hide show
  1. package/README.md +13 -1
  2. package/dist/{acp-adapter-9n319wqc.js → acp-adapter-4jncb126.js} +12 -5
  3. package/dist/{actions-2vxqvpr9.js → actions-31vp3ae7.js} +10 -9
  4. package/dist/{app-w066xfy0.js → app-1hh67x8w.js} +6 -6
  5. package/dist/{assistant-kt31dj2h.js → assistant-z66dmz6p.js} +13 -11
  6. package/dist/{boot-reembed-az5rassp.js → boot-reembed-fqzxxzjh.js} +7 -7
  7. package/dist/{boot-reembed-j3mm3rfz.js → boot-reembed-wxa8ya0s.js} +8 -8
  8. package/dist/{boot-scrub-logs-b6h54817.js → boot-scrub-logs-jnyndh79.js} +5 -5
  9. package/dist/{claude-adapter-w1z04ann.js → claude-adapter-3qpa9w0j.js} +9 -9
  10. package/dist/{claude-managed-adapter-sactwn31.js → claude-managed-adapter-qvsmh6mv.js} +2 -2
  11. package/dist/{claude-sdk-session-f0cc1kec.js → claude-sdk-session-2yxgjs5d.js} +9 -9
  12. package/dist/{cli-y6b6fb76.js → cli-08b07b4r.js} +1 -1
  13. package/dist/{cli-8fc8de14.js → cli-0z2v4nhw.js} +1 -1
  14. package/dist/{cli-de14znh8.js → cli-19f354qr.js} +2 -2
  15. package/dist/{cli-hbv0kq6w.js → cli-2qts1hys.js} +3 -3
  16. package/dist/{cli-kpv03zhj.js → cli-3apbzgyk.js} +27 -15
  17. package/dist/{cli-1th1728g.js → cli-3q5ejx9p.js} +2 -2
  18. package/dist/{cli-d8brzxjb.js → cli-4wtq5jjv.js} +2 -2
  19. package/dist/{cli-ct0et58h.js → cli-5my3bjsd.js} +1 -1
  20. package/dist/{cli-cn6mmc7f.js → cli-6664w7y4.js} +1 -1
  21. package/dist/{cli-yz3djrm0.js → cli-6pqcb4q0.js} +53 -13
  22. package/dist/{cli-719k9j2c.js → cli-7smrsr25.js} +3 -3
  23. package/dist/{cli-rw51bq3j.js → cli-9fy6nk6g.js} +1 -1
  24. package/dist/{cli-zhjmqxzh.js → cli-9hspd3dp.js} +2 -2
  25. package/dist/{cli-3tgwnf43.js → cli-aae6hz6a.js} +4 -4
  26. package/dist/{cli-89xfd7f9.js → cli-cr12paw2.js} +5 -5
  27. package/dist/{cli-b5bq9crk.js → cli-d8ftsp62.js} +8 -8
  28. package/dist/{cli-4nr988h6.js → cli-e95cx1eb.js} +3 -3
  29. package/dist/{cli-n8508nre.js → cli-gjhjfeeg.js} +1 -1
  30. package/dist/{cli-p1d5b073.js → cli-gv88e1vf.js} +1 -1
  31. package/dist/{cli-rvqz34y5.js → cli-jecc01cb.js} +1 -1
  32. package/dist/{cli-8v0g1yc8.js → cli-k0cr4kat.js} +1 -1
  33. package/dist/{cli-mmemxxdc.js → cli-kgz4np97.js} +4 -4
  34. package/dist/{cli-w9kb3bk1.js → cli-kjwt6hdf.js} +3 -1
  35. package/dist/{cli-f7dsy61y.js → cli-mnmsfd1w.js} +6 -2
  36. package/dist/{cli-54wwve05.js → cli-n3dz2942.js} +37 -26
  37. package/dist/{cli-12fz972k.js → cli-nye12xk5.js} +8 -5
  38. package/dist/{cli-tzvk9haz.js → cli-phsxnkp6.js} +20 -20
  39. package/dist/{cli-53s590z8.js → cli-q50ef8g0.js} +1 -1
  40. package/dist/{cli-f146mn5s.js → cli-qaacms9y.js} +7 -6
  41. package/dist/{cli-xqcq9y5e.js → cli-r2ap2czm.js} +1 -0
  42. package/dist/{cli-m8yrsg97.js → cli-sd5pv3b1.js} +3 -3
  43. package/dist/{cli-0b6y7kxm.js → cli-xm87hzbw.js} +1 -1
  44. package/dist/{cli-ftht3zzj.js → cli-yab68w40.js} +285 -11
  45. package/dist/{cli-atcve9y8.js → cli-yb9qhqam.js} +227 -59
  46. package/dist/{cli-smby5m17.js → cli-ygweqcn4.js} +133 -22
  47. package/dist/{cli-dttph1bw.js → cli-ykxrnd1q.js} +2 -2
  48. package/dist/{cli-m9sxbkhm.js → cli-zvz62chv.js} +3 -3
  49. package/dist/cli.js +16 -14
  50. package/dist/{codex-adapter-zw4dwsx7.js → codex-adapter-8meawydj.js} +4 -4
  51. package/dist/{codex-hook-bw8p5nh4.js → codex-hook-hfrt9shr.js} +2 -2
  52. package/dist/{codex-session-runner-k981s65v.js → codex-session-runner-gjgxyp5j.js} +4 -4
  53. package/dist/{commands-sykrxx7e.js → commands-hz3nb95f.js} +5 -5
  54. package/dist/{db-sfbsc8wt.js → db-ggz97zbm.js} +5 -5
  55. package/dist/{e2b-6hb103d8.js → e2b-p04vmtx0.js} +1 -1
  56. package/dist/{handlers-0aspcejm.js → handlers-nbzdzn9k.js} +14 -11
  57. package/dist/{hook-hxccs7m5.js → hook-bks2q1f6.js} +7 -7
  58. package/dist/{hook-4nvqj2sb.js → hook-kq9wp0ep.js} +8 -8
  59. package/dist/{http-y06z517d.js → http-hrf77v87.js} +96 -45
  60. package/dist/{index-tce1rss4.js → index-6nsbg88z.js} +443 -144
  61. package/dist/{index-x5j894zf.js → index-8hmeg8j1.js} +14 -14
  62. package/dist/{index-k8znfx0z.js → index-cqtebbhb.js} +13 -13
  63. package/dist/{index-hssww7kj.js → index-n28pw1zt.js} +15 -15
  64. package/dist/{keepalive-w733ax66.js → keepalive-dxc12gvg.js} +7 -7
  65. package/dist/{lead-75bjqa9d.js → lead-yckfn18d.js} +32 -32
  66. package/dist/{maintenance-w4f1zjck.js → maintenance-6kfb2m5e.js} +8 -8
  67. package/dist/{oauth-refresh-sweep-wdetky26.js → oauth-refresh-sweep-1x1ac722.js} +6 -6
  68. package/dist/{onboard-bb5net8s.js → onboard-dcyt1609.js} +4 -3
  69. package/dist/{opencode-adapter-fef1mrn2.js → opencode-adapter-dce4wskd.js} +2 -2
  70. package/dist/{otel-impl-0w7c14ap.js → otel-impl-tbk3e5dr.js} +2 -2
  71. package/dist/{pi-mono-adapter-2has4gnp.js → pi-mono-adapter-zvshk5cj.js} +2 -2
  72. package/dist/{pricing-refresh-9nzsrgcq.js → pricing-refresh-kaqg567e.js} +7 -7
  73. package/dist/{rbac-roles-z1jrffbt.js → rbac-roles-ammq6rd2.js} +5 -5
  74. package/dist/{rbac-roles-r3hqjp09.js → rbac-roles-cf97v1vd.js} +6 -6
  75. package/dist/{render-v2-vbr89fnb.js → render-v2-80w603vy.js} +6 -6
  76. package/dist/{seed-pricing-g62hy1pk.js → seed-pricing-zbrn83xv.js} +6 -6
  77. package/dist/{setup-gnfqnptp.js → setup-fe66kczs.js} +2 -2
  78. package/dist/{worker-kznxp4qm.js → worker-qmb2d1e2.js} +32 -32
  79. package/dist/{x-n2phr5vm.js → x-n3e0v3xa.js} +2 -2
  80. package/openapi.json +46 -4
  81. package/package.json +3 -1
  82. package/src/agentmail/handlers.ts +5 -0
  83. package/src/automation-preflight-alert.ts +41 -0
  84. package/src/be/automation-preflight.ts +3 -3
  85. package/src/be/budget-refusal-notify.ts +1 -0
  86. package/src/be/db/tasks/read.ts +5 -0
  87. package/src/be/db.ts +132 -2
  88. package/src/be/migrations/150_deferred_task_waits.sql +18 -0
  89. package/src/be/migrations/151_repair_scheduled_task_required_params.sql +39 -0
  90. package/src/be/migrations/152_routing_decisions.sql +44 -0
  91. package/src/be/scripts/typecheck.ts +33 -22
  92. package/src/be/seed-scripts/catalog/delegate.ts +5 -1
  93. package/src/be/seed-skills/bundled-files.generated.json +10 -0
  94. package/src/be/seed-skills/index.ts +3 -0
  95. package/src/be/steering.ts +1 -0
  96. package/src/be/swarm-config-guard.ts +11 -0
  97. package/src/commands/onboard/steps/post-task.tsx +1 -0
  98. package/src/github/handlers.ts +9 -0
  99. package/src/gitlab/handlers.ts +4 -0
  100. package/src/heartbeat/heartbeat.ts +8 -0
  101. package/src/http/approval-requests.ts +1 -0
  102. package/src/http/apps.ts +1 -0
  103. package/src/http/config.ts +15 -6
  104. package/src/http/mcp.ts +31 -4
  105. package/src/http/script-runs.ts +6 -0
  106. package/src/http/tasks.ts +52 -30
  107. package/src/integrations/kapso/inbound.ts +1 -0
  108. package/src/jira/sync.ts +2 -0
  109. package/src/linear/sync.ts +2 -0
  110. package/src/prompts/session-templates.ts +29 -15
  111. package/src/scheduler/deferred-task-waits.ts +123 -0
  112. package/src/scheduler/schedule-task.ts +42 -0
  113. package/src/scheduler/scheduler.ts +22 -31
  114. package/src/script-workflows/workflow-ctx.ts +2 -0
  115. package/src/scripts-runtime/types/stdlib.d.ts +38 -24
  116. package/src/scripts-runtime/types/swarm-sdk.d.ts +38 -24
  117. package/src/server.ts +10 -1
  118. package/src/slack/actions.ts +1 -0
  119. package/src/slack/assistant.ts +2 -0
  120. package/src/slack/handlers.ts +3 -0
  121. package/src/slack/thread-buffer.ts +2 -0
  122. package/src/tasks/worker-follow-up.ts +9 -0
  123. package/src/tests/acp-adapter.test.ts +35 -6
  124. package/src/tests/acp-session-token.test.ts +67 -0
  125. package/src/tests/asset-key-api.test.ts +15 -2
  126. package/src/tests/asset-key-mcp.test.ts +2 -0
  127. package/src/tests/automation-preflight-alert.test.ts +91 -0
  128. package/src/tests/claude-worker-parity.test.ts +3 -0
  129. package/src/tests/config-session-auth.test.ts +143 -0
  130. package/src/tests/defer-task.test.ts +117 -6
  131. package/src/tests/deferred-task-wake.test.ts +456 -0
  132. package/src/tests/heartbeat-reroute-decision.test.ts +1 -0
  133. package/src/tests/http-api-integration.test.ts +33 -4
  134. package/src/tests/mcp-input-ergonomics.test.ts +386 -0
  135. package/src/tests/model-control.test.ts +18 -5
  136. package/src/tests/opencode-adapter.test.ts +2 -2
  137. package/src/tests/promote-draft-task-route.test.ts +4 -0
  138. package/src/tests/prompt-template-session.test.ts +13 -4
  139. package/src/tests/rbac-wire-e2e.test.ts +2 -2
  140. package/src/tests/routing-decision-persistence.test.ts +520 -0
  141. package/src/tests/routing-reason-contract.test.ts +89 -0
  142. package/src/tests/routing-reason-inventory.test.ts +55 -0
  143. package/src/tests/schedule-target-type.test.ts +48 -1
  144. package/src/tests/scheduled-task-required-params-migration.test.ts +182 -0
  145. package/src/tests/scheduled-tasks.test.ts +9 -5
  146. package/src/tests/script-connections.test.ts +4 -0
  147. package/src/tests/script-runs-http.test.ts +31 -0
  148. package/src/tests/scripts-typecheck.test.ts +11 -0
  149. package/src/tests/secret-scrubber.test.ts +12 -0
  150. package/src/tests/send-task-output-schema.test.ts +66 -1
  151. package/src/tests/send-task-requested-by.test.ts +1 -0
  152. package/src/tests/send-task-slack-routing-guard.test.ts +2 -0
  153. package/src/tests/store-progress-blocked-waiting-gate.test.ts +151 -0
  154. package/src/tests/swarm-tool-result-gate.test.ts +24 -0
  155. package/src/tests/task-tool-manifest.test.ts +47 -0
  156. package/src/tests/task-tool-preload-mcp.test.ts +125 -0
  157. package/src/tests/task-tools-ctx.test.ts +2 -0
  158. package/src/tests/tool-output-agent-id.test.ts +1 -0
  159. package/src/tests/ui-followup-lead-delegation.test.ts +6 -1
  160. package/src/tests/workflow-agent-task.test.ts +24 -1
  161. package/src/tests/workflow-engine-v2.test.ts +58 -2
  162. package/src/tools/accept-steer.ts +0 -1
  163. package/src/tools/defer-task.ts +73 -21
  164. package/src/tools/memory-get.ts +10 -3
  165. package/src/tools/memory-rate.ts +19 -7
  166. package/src/tools/memory-search.ts +2 -1
  167. package/src/tools/memory-store.ts +25 -5
  168. package/src/tools/send-task.ts +40 -2
  169. package/src/tools/skills/skill-publish.ts +1 -0
  170. package/src/tools/store-progress.ts +78 -4
  171. package/src/tools/templates.ts +2 -1
  172. package/src/tools/utils.ts +33 -2
  173. package/src/types.ts +14 -1
  174. package/src/utils/acp-session-token.ts +14 -4
  175. package/src/utils/secret-scrubber.ts +2 -0
  176. package/src/utils/task-tool-manifest.ts +33 -0
  177. package/src/workflows/engine.ts +4 -1
  178. package/src/workflows/executors/agent-task.ts +7 -1
  179. package/templates/ai-toolbox.manifest.json +6 -1
  180. package/templates/skills/comms/config.json +18 -0
  181. package/templates/skills/comms/content.md +74 -0
  182. package/templates/skills/comms/files/references/ste-rules.md +73 -0
  183. package/templates/skills/comms/files/references/visual-shapes.md +112 -0
  184. package/templates/skills/swarm-scripts/SKILL.md +3 -2
  185. package/templates/skills/swarm-scripts/content.md +3 -2
@@ -4,12 +4,14 @@ import * as z from "zod";
4
4
  import { resolveTaskAuditUserId } from "@/be/audit-user";
5
5
  import {
6
6
  completeTask,
7
+ createLogEntry,
7
8
  createScheduledTask,
8
9
  getAgentById,
9
10
  getDbClient,
10
11
  getTaskById,
11
12
  updateAgentStatusFromCapacity,
12
13
  } from "@/be/db";
14
+ import { reconcileDeferredTaskWaits } from "@/scheduler/deferred-task-waits";
13
15
  import { runTaskTerminalEffects } from "@/tasks/task-terminal-effects";
14
16
  import { getTaskOutputValidationError } from "@/tasks/terminal-result-guard";
15
17
  import { assertOwnsTask, ownerCtx } from "@/tools/task-tool-ctx";
@@ -32,7 +34,7 @@ export const registerDeferTaskTool = (server: McpServer) => {
32
34
  title: "Defer Task",
33
35
  annotations: { destructiveHint: false, idempotentHint: false },
34
36
  description:
35
- "Completes this task now with status `completed` and books a wake-up for you. Use when the result needs time: a build, a deploy, a reply. The task reaches its final state on this call; the lead sees your summary as its output. A one-off schedule wakes you up later with a child task that carries this task as its parent. Provide delayMs or runAt, a summary of what you did, and a note that says what is pending and what to check.",
37
+ "Completes this task now with status `completed` and books a wake-up for you. Use when the result needs time: a build, a deploy, a reply. The task reaches its final state on this call; the lead sees your summary as its output unless the task has an outputSchema. For a task with an outputSchema, provide output as a JSON string matching that schema; it is stored verbatim as terminal output, while deferral details remain visible in the task log. A one-off schedule wakes you up later with a child task that carries this task as its parent. Optionally provide wakeOn to wake early when another task completes or fails; delayMs or runAt remains required as the ceiling. Provide delayMs or runAt, a summary of what you did, and a note that says what is pending and what to check.",
36
38
  inputSchema: z.object({
37
39
  taskId: z.string().describe("The ID of the task you are working on."),
38
40
  delayMs: z
@@ -46,12 +48,28 @@ export const registerDeferTaskTool = (server: McpServer) => {
46
48
  .datetime()
47
49
  .optional()
48
50
  .describe("Wake up at this ISO datetime (e.g. '2026-03-06T15:00:00Z'). Must be future."),
51
+ wakeOn: z
52
+ .object({
53
+ event: z.enum(["task.completed", "task.failed", "settled"]),
54
+ taskId: z.string().min(1),
55
+ })
56
+ .strict()
57
+ .optional()
58
+ .describe(
59
+ "Wake early on this task event. settled covers completed or failed. Already-terminal tasks are rejected; a delayMs/runAt ceiling is still required.",
60
+ ),
49
61
  summary: z
50
62
  .string()
51
63
  .min(1)
52
64
  .max(4000)
53
65
  .describe(
54
- "What you did so far and where things stand. This becomes the task's output; the lead and your wake-up run both read it.",
66
+ "What you did so far and where things stand. Stored in the task log for tasks with an outputSchema; otherwise becomes the task's output.",
67
+ ),
68
+ output: z
69
+ .string()
70
+ .optional()
71
+ .describe(
72
+ "Required when the task has an outputSchema: a JSON string matching that schema, stored verbatim as terminal output. Ignored for tasks without an outputSchema.",
55
73
  ),
56
74
  note: z
57
75
  .string()
@@ -71,7 +89,11 @@ export const registerDeferTaskTool = (server: McpServer) => {
71
89
  nextRunAt: z.string().optional(),
72
90
  }),
73
91
  },
74
- async ({ taskId, delayMs, runAt, summary, note, checks }, requestInfo, _meta) => {
92
+ async (
93
+ { taskId, delayMs, runAt, wakeOn, summary, output, note, checks },
94
+ requestInfo,
95
+ _meta,
96
+ ) => {
75
97
  if (!requestInfo.agentId) {
76
98
  return toolErr('Agent ID not found. Set the "X-Agent-ID" header.');
77
99
  }
@@ -122,26 +144,37 @@ export const registerDeferTaskTool = (server: McpServer) => {
122
144
  }
123
145
  const nextRunAt = delayMs ? new Date(Date.now() + delayMs).toISOString() : runAt!;
124
146
 
147
+ const wakeDescription = wakeOn
148
+ ? `on ${wakeOn.event} for task ${wakeOn.taskId}, or by ${nextRunAt}`
149
+ : `at ${nextRunAt}`;
125
150
  const checksBlock = renderChecks(checks);
126
151
  const taskTemplate = `Resume task ${taskId}: ${note}${checksBlock}`;
127
152
  const createdBy =
128
153
  (await resolveTaskAuditUserId(requestInfo.sourceTaskId, requestInfo.agentId)) ?? undefined;
129
154
 
130
- // The deferral note is written as the task's FINAL output — a task with
131
- // an outputSchema must satisfy it on completion (store-progress enforces
132
- // the same rule). Validate before creating anything: the real schedule
133
- // id isn't known yet, but it never changes whether this prose is valid
134
- // JSON against the schema, so a placeholder stands in for it here.
135
- const previewOutput = `${summary}\n\nDeferred until ${nextRunAt} (schedule pending). Pending: ${note}${checksBlock}`;
136
- const outputValidationError = getTaskOutputValidationError(task.outputSchema, previewOutput);
137
- if (outputValidationError) {
138
- return toolErr(
139
- `Task ${taskId} has an outputSchema; its terminal output must satisfy it, but a deferral note cannot. ${outputValidationError}`,
140
- );
155
+ // Validate before creating the schedule or changing the task.
156
+ if (task.outputSchema) {
157
+ if (!output) {
158
+ return toolErr(
159
+ `Task ${taskId} has an outputSchema. Call defer-task with output: a JSON string matching that schema. Summary and note are stored separately in the task log.`,
160
+ );
161
+ }
162
+ const outputValidationError = getTaskOutputValidationError(task.outputSchema, output);
163
+ if (outputValidationError) return toolErr(outputValidationError);
141
164
  }
142
165
 
143
166
  try {
144
167
  const committed = await getDbClient().transaction(async () => {
168
+ if (wakeOn) {
169
+ if (wakeOn.taskId === taskId)
170
+ throw new DeferAbortedError("Cannot wake on the task being deferred.");
171
+ const watched = await getTaskById(wakeOn.taskId);
172
+ if (!watched) throw new DeferAbortedError(`Watched task ${wakeOn.taskId} not found.`);
173
+ if (isTerminalTaskStatus(watched.status))
174
+ throw new DeferAbortedError(
175
+ `Watched task ${wakeOn.taskId} is already ${watched.status}; read its result instead of deferring.`,
176
+ );
177
+ }
145
178
  const schedule = await createScheduledTask({
146
179
  // Unique name (`getScheduledTaskByName` is a unique lookup). The UUID
147
180
  // prevents concurrent deferrals of the same task from colliding.
@@ -165,12 +198,22 @@ export const registerDeferTaskTool = (server: McpServer) => {
165
198
  createdBy,
166
199
  });
167
200
 
168
- const output = `${summary}\n\nDeferred until ${nextRunAt} (schedule ${schedule.id}). Pending: ${note}${checksBlock}`;
201
+ if (wakeOn) {
202
+ await getDbClient().run(
203
+ "INSERT INTO deferred_task_waits (scheduleId, taskId, eventName, created_by, updated_by) VALUES (?, ?, ?, ?, ?)",
204
+ [schedule.id, wakeOn.taskId, wakeOn.event, createdBy ?? null, createdBy ?? null],
205
+ );
206
+ getDbClient().afterCommit(() => {
207
+ void reconcileDeferredTaskWaits(wakeOn.taskId).catch((err) => {
208
+ console.error("[defer-task] Event wake reconciliation failed:", err);
209
+ });
210
+ });
211
+ }
212
+
213
+ const deferralDetails = `${summary}\n\nDeferred until ${nextRunAt} (schedule ${schedule.id}). Pending: ${note}${checksBlock}`;
169
214
 
170
- // Deliberately NOT running `getTaskOutputValidationError` again here:
171
- // already validated above against a placeholder id; re-run would be
172
- // redundant since the id never affects JSON-shape validity.
173
- const completed = await completeTask(taskId, output);
215
+ const terminalOutput = task.outputSchema ? output! : deferralDetails;
216
+ const completed = await completeTask(taskId, terminalOutput);
174
217
  if (!completed) {
175
218
  // Another writer terminally completed/failed/cancelled this task
176
219
  // between our early check and this transaction's write. Abort:
@@ -182,6 +225,15 @@ export const registerDeferTaskTool = (server: McpServer) => {
182
225
  );
183
226
  }
184
227
 
228
+ if (task.outputSchema) {
229
+ await createLogEntry({
230
+ eventType: "task_progress",
231
+ taskId,
232
+ agentId: requestInfo.agentId,
233
+ newValue: deferralDetails,
234
+ });
235
+ }
236
+
185
237
  // afterCommit: the transaction can still roll back; business-use must
186
238
  // not be told the task completed for a write that never landed.
187
239
  getDbClient().afterCommit(() => {
@@ -207,7 +259,7 @@ export const registerDeferTaskTool = (server: McpServer) => {
207
259
  await updateAgentStatusFromCapacity(task.agentId);
208
260
  }
209
261
 
210
- return { scheduleId: schedule.id, output, completed };
262
+ return { scheduleId: schedule.id, output: terminalOutput, completed };
211
263
  });
212
264
 
213
265
  await runTaskTerminalEffects({
@@ -218,7 +270,7 @@ export const registerDeferTaskTool = (server: McpServer) => {
218
270
  });
219
271
 
220
272
  return toolOk(
221
- `Task ${taskId} completed and deferred. Wake-up at ${nextRunAt} (schedule ${committed.scheduleId}). This task is final; the wake-up task continues the work.`,
273
+ `Task ${taskId} completed and deferred. Wake-up ${wakeDescription} (schedule ${committed.scheduleId}). This task is final; the wake-up task continues the work.`,
222
274
  {
223
275
  data: {
224
276
  yourAgentId: requestInfo.agentId,
@@ -80,12 +80,14 @@ export const registerMemoryGetTool = (server: McpServer) => {
80
80
  annotations: { readOnlyHint: true },
81
81
 
82
82
  inputSchema: z.object({
83
- memoryId: z.uuid().describe("The ID of the memory to retrieve."),
83
+ memoryId: z.uuid().optional().describe("The ID of the memory to retrieve (or use id)."),
84
+ id: z.uuid().optional().describe("Alias for memoryId, matching memory-search results."),
84
85
  intent: z
85
86
  .string()
86
87
  .min(1)
88
+ .optional()
87
89
  .describe(
88
- "Why you are retrieving this memory. Required. E.g. 'need full details of the auth fix pattern'.",
90
+ "Optional reason for retrieving this memory. E.g. 'need full details of the auth fix pattern'.",
89
91
  ),
90
92
  }),
91
93
  outputSchema: swarmToolOutputSchema({
@@ -102,7 +104,12 @@ export const registerMemoryGetTool = (server: McpServer) => {
102
104
  rateHint: z.string().optional(),
103
105
  }),
104
106
  },
105
- async ({ memoryId, intent }, requestInfo, _meta) => {
107
+ async ({ memoryId: requestedMemoryId, id, intent }, requestInfo, _meta) => {
108
+ if (requestedMemoryId && id && requestedMemoryId !== id) {
109
+ return toolErr("memoryId and id must identify the same memory; supply only one.");
110
+ }
111
+ const memoryId = requestedMemoryId ?? id;
112
+ if (!memoryId) return toolErr("Supply memoryId (or its alias id) from memory-search.");
106
113
  const store = getMemoryStore();
107
114
  const memoryForAuth = await store.peek(memoryId);
108
115
 
@@ -42,15 +42,21 @@ export const registerMemoryRateTool = (server: McpServer) => {
42
42
  "the swarm learns to surface better memories next time.",
43
43
  annotations: { destructiveHint: false },
44
44
  inputSchema: z.object({
45
- id: z.string().describe("Memory ID returned by memory_search."),
45
+ id: z
46
+ .string()
47
+ .min(1)
48
+ .optional()
49
+ .describe("Memory ID returned by memory-search (or use memoryId)."),
50
+ memoryId: z.string().min(1).optional().describe("Alias for id, matching memory-get."),
46
51
  useful: z
47
52
  .boolean()
48
53
  .describe("true = this memory helped solve the task; false = misled or wasted time."),
49
54
  note: z
50
55
  .string()
51
- .max(280)
52
56
  .optional()
53
- .describe("Short reason. Captured for telemetry; not surfaced to other agents."),
57
+ .describe(
58
+ "Reason, stored up to 500 characters for telemetry; not surfaced to other agents.",
59
+ ),
54
60
  referencesSource: z
55
61
  .string()
56
62
  .min(1)
@@ -60,7 +66,13 @@ export const registerMemoryRateTool = (server: McpServer) => {
60
66
  }),
61
67
  outputSchema: swarmToolOutputSchema(),
62
68
  },
63
- async ({ id, useful, note, referencesSource }, requestInfo, _meta) => {
69
+ async ({ id, memoryId, useful, note, referencesSource }, requestInfo, _meta) => {
70
+ if (id && memoryId && id !== memoryId) {
71
+ return toolErr("id and memoryId must identify the same memory; supply only one.");
72
+ }
73
+ const resolvedMemoryId = id ?? memoryId;
74
+ if (!resolvedMemoryId)
75
+ return toolErr("Supply id (or its alias memoryId) from memory-search.");
64
76
  if (!requestInfo.agentId) {
65
77
  return toolErr("Agent ID required. Are you registered in the swarm?");
66
78
  }
@@ -85,11 +97,11 @@ export const registerMemoryRateTool = (server: McpServer) => {
85
97
  const apiKey = getApiKey();
86
98
 
87
99
  const event = {
88
- memoryId: id,
100
+ memoryId: resolvedMemoryId,
89
101
  signal: useful ? 1 : -1,
90
102
  weight: 1.0,
91
103
  source: "explicit-self" as const,
92
- reasoning: note ?? "",
104
+ reasoning: (note ?? "").slice(0, 500),
93
105
  taskId: requestInfo.sourceTaskId,
94
106
  ...(cleanedReferencesSource !== undefined
95
107
  ? { referencesSource: cleanedReferencesSource }
@@ -129,7 +141,7 @@ export const registerMemoryRateTool = (server: McpServer) => {
129
141
  return toolErr(`Memory rating failed (HTTP ${response.status}).`);
130
142
  }
131
143
 
132
- return toolOk(`Memory ${id} rated as ${useful ? "useful" : "not useful"}.`);
144
+ return toolOk(`Memory ${resolvedMemoryId} rated as ${useful ? "useful" : "not useful"}.`);
133
145
  } catch (err) {
134
146
  return toolErr(`Memory rating failed: ${(err as Error).message}`);
135
147
  }
@@ -84,8 +84,9 @@ export const registerMemorySearchTool = (server: McpServer) => {
84
84
  intent: z
85
85
  .string()
86
86
  .min(1)
87
+ .optional()
87
88
  .describe(
88
- "Why you are searching for this memory. Required. E.g. 'looking for auth pattern to fix login bug'.",
89
+ "Optional reason for searching for this memory. E.g. 'looking for auth pattern to fix login bug'.",
89
90
  ),
90
91
  scope: z
91
92
  .enum(["all", "agent", "swarm"])
@@ -24,14 +24,19 @@ export const registerMemoryStoreTool = (server: McpServer) => {
24
24
  .string()
25
25
  .min(1)
26
26
  .max(200)
27
- .describe("Short title, one line, used in search results and the UI."),
27
+ .optional()
28
+ .describe(
29
+ "Short title used in search results and the UI. Defaults to the first non-empty content line (up to 200 characters).",
30
+ ),
28
31
  scope: AgentMemoryScopeSchema.default("agent").describe(
29
32
  "'agent' (default): only you can recall it. 'swarm': every agent can recall it.",
30
33
  ),
31
34
  tags: z
32
- .array(z.string())
35
+ .union([z.array(z.string()), z.string()])
33
36
  .optional()
34
- .describe("Free-form tags, for example a repo name or a topic."),
37
+ .describe(
38
+ "Free-form tags as an array or a comma-separated string, for example a repo name or a topic.",
39
+ ),
35
40
  taskId: z
36
41
  .uuid()
37
42
  .optional()
@@ -49,7 +54,22 @@ export const registerMemoryStoreTool = (server: McpServer) => {
49
54
  queued: z.boolean().optional(),
50
55
  }),
51
56
  },
52
- async ({ content, name, scope, tags, taskId, intent }, requestInfo, _meta) => {
57
+ async ({ content, name: requestedName, scope, tags, taskId, intent }, requestInfo, _meta) => {
58
+ const name =
59
+ requestedName ??
60
+ content
61
+ .split(/\r?\n/)
62
+ .find((line) => line.trim())
63
+ ?.trim()
64
+ .slice(0, 200) ??
65
+ "Untitled memory";
66
+ const normalizedTags =
67
+ typeof tags === "string"
68
+ ? tags
69
+ .split(",")
70
+ .map((tag) => tag.trim())
71
+ .filter(Boolean)
72
+ : tags;
53
73
  if (!requestInfo.agentId) {
54
74
  return toolErr("Agent ID required. Are you registered in the swarm?");
55
75
  }
@@ -64,7 +84,7 @@ export const registerMemoryStoreTool = (server: McpServer) => {
64
84
  scope,
65
85
  source: "manual",
66
86
  sourceTaskId: taskId ?? null,
67
- tags,
87
+ tags: normalizedTags,
68
88
  intent,
69
89
  });
70
90
 
@@ -11,6 +11,7 @@ import {
11
11
  getAgentById,
12
12
  getDbClient,
13
13
  getTaskById,
14
+ getUserById,
14
15
  hasCapacity,
15
16
  } from "@/be/db";
16
17
  import { repointTrackerSyncBySwarmId } from "@/be/db-queries/tracker";
@@ -30,6 +31,7 @@ import {
30
31
  FollowUpConfigSchema,
31
32
  ModelTierSchema,
32
33
  ReasoningEffortSchema,
34
+ RoutingReasonSchema,
33
35
  splitLegacyModelAlias,
34
36
  } from "@/types";
35
37
  import { findJsonSchemaShapeErrors } from "@/workflows/json-schema-validator";
@@ -60,6 +62,14 @@ export const sendTaskInputSchema = z
60
62
  .string()
61
63
  .optional()
62
64
  .describe("The agent to assign/offer task to. Omit to create unassigned task for pool."),
65
+ routingReason: RoutingReasonSchema.optional().describe(
66
+ "Why this agent was selected. Required when agentId is supplied; omit for pool routing.",
67
+ ),
68
+ routingNote: z
69
+ .string()
70
+ .max(200)
71
+ .optional()
72
+ .describe("Optional routing context (maximum 200 characters)."),
63
73
  task: z.string().min(1).describe("The task description to send."),
64
74
  key: AssetKeySchema.optional().describe(
65
75
  "Logical namespace key. Child tasks inherit their parent namespace when provided.",
@@ -148,10 +158,10 @@ export const sendTaskInputSchema = z
148
158
  ),
149
159
  requestedByUserId: z
150
160
  .string()
151
- .uuid()
161
+ .regex(/^[a-f0-9]{32}$/, "Expected a registry user ID (32 lowercase hexadecimal characters).")
152
162
  .optional()
153
163
  .describe(
154
- "ID of the human user who originally requested this task chain. When omitted, inherited from the caller's current task so the attribution flows through multi-hop delegation automatically.",
164
+ "Registered requester ID (32 lowercase hexadecimal characters). When omitted, inherited from the caller's current task so the attribution flows through multi-hop delegation automatically.",
155
165
  ),
156
166
  followUpConfig: FollowUpConfigSchema.optional().describe(
157
167
  "Control the lead follow-up created when this task finishes. When to use `followUpConfig`: set `disabled: true` when you'll wait for this task to complete inline and no follow-up is needed; set `onCompleted` / `onFailed` with specific instructions when you need to follow up effectively on a particular outcome of a long-running flow; for normal one-shot tasks, leave it unset because defaults are fine. It is most valuable for long-running / complex flows.",
@@ -173,6 +183,13 @@ export const sendTaskInputSchema = z
173
183
  path: [hasChannel ? "slackThreadTs" : "slackChannelId"],
174
184
  });
175
185
  }
186
+ if (data.agentId !== undefined && data.routingReason === undefined) {
187
+ ctx.addIssue({
188
+ code: z.ZodIssueCode.custom,
189
+ message: "routingReason is required when agentId is supplied.",
190
+ path: ["routingReason"],
191
+ });
192
+ }
176
193
  checkOutputSchemaShape(data.outputSchema, ctx);
177
194
  });
178
195
 
@@ -221,6 +238,8 @@ export async function sendTaskHandler(
221
238
  ctx: ToolCtx,
222
239
  {
223
240
  agentId,
241
+ routingReason,
242
+ routingNote,
224
243
  task,
225
244
  key,
226
245
  offerMode,
@@ -246,6 +265,10 @@ export async function sendTaskHandler(
246
265
  outputSchema,
247
266
  }: SendTaskArgs,
248
267
  ): Promise<SwarmToolResult> {
268
+ // Defense in depth for direct TypeScript callers that bypass MCP schema parsing.
269
+ if (agentId !== undefined && routingReason === undefined) {
270
+ return toolErr("routingReason is required when agentId is supplied.");
271
+ }
249
272
  if (ctx.kind === "owner" && !ctx.agentId) {
250
273
  return toolErr('Agent ID not found. The MCP client should define the "X-Agent-ID" header.', {
251
274
  data: { yourAgentId: ctx.agentId },
@@ -257,6 +280,12 @@ export async function sendTaskHandler(
257
280
  const requestedByUserId =
258
281
  ctx.kind === "user" ? ctx.userId : (inputRequestedByUserId ?? undefined);
259
282
 
283
+ if (ctx.kind === "owner" && requestedByUserId && !(await getUserById(requestedByUserId))) {
284
+ return toolErr("requestedByUserId must identify an existing registered user.", {
285
+ data: { yourAgentId: creatorAgentId },
286
+ });
287
+ }
288
+
260
289
  if (ctx.kind === "owner" && agentId === ctx.agentId) {
261
290
  return toolErr("Cannot send a task to yourself, are you drunk?", {
262
291
  data: { yourAgentId: ctx.agentId },
@@ -332,6 +361,9 @@ export async function sendTaskHandler(
332
361
  effectiveAgentId = effectiveParentTask.agentId;
333
362
  }
334
363
  }
364
+ const effectiveRoutingReason =
365
+ agentId !== undefined ? routingReason : effectiveAgentId ? "continuity" : undefined;
366
+ const effectiveRoutingNote = effectiveRoutingReason ? routingNote : undefined;
335
367
 
336
368
  // The three dedup guards are pure reads, so they run twice: once here as a
337
369
  // fast path (keeping this tool's existing early-exit responses), and once
@@ -463,6 +495,8 @@ export async function sendTaskHandler(
463
495
  overrideSlackContext,
464
496
  followUpConfig,
465
497
  outputSchema,
498
+ routingReason: effectiveRoutingReason,
499
+ routingNote: effectiveRoutingNote,
466
500
  routingAffinity:
467
501
  effectiveLeadOnly || requiredCapabilities?.length
468
502
  ? { leadOnly: effectiveLeadOnly, capabilities: requiredCapabilities ?? [] }
@@ -530,6 +564,8 @@ export async function sendTaskHandler(
530
564
  overrideSlackContext,
531
565
  followUpConfig,
532
566
  outputSchema,
567
+ routingReason: effectiveRoutingReason,
568
+ routingNote: effectiveRoutingNote,
533
569
  routingAffinity:
534
570
  effectiveLeadOnly || requiredCapabilities?.length
535
571
  ? { leadOnly: effectiveLeadOnly, capabilities: requiredCapabilities ?? [] }
@@ -571,6 +607,8 @@ export async function sendTaskHandler(
571
607
  overrideSlackContext,
572
608
  followUpConfig,
573
609
  outputSchema,
610
+ routingReason: effectiveRoutingReason,
611
+ routingNote: effectiveRoutingNote,
574
612
  routingAffinity:
575
613
  effectiveLeadOnly || requiredCapabilities?.length
576
614
  ? { leadOnly: effectiveLeadOnly, capabilities: requiredCapabilities ?? [] }
@@ -71,6 +71,7 @@ To reject: close this task with a rejection reason.`;
71
71
 
72
72
  const task = await createTaskExtended(taskDescription, {
73
73
  agentId: leadAgent.id,
74
+ routingReason: "skill",
74
75
  creatorAgentId: requestInfo.agentId,
75
76
  source: "mcp",
76
77
  taskType: "skill-approval",
@@ -29,6 +29,17 @@ import { scrubSecrets } from "@/utils/secret-scrubber";
29
29
  // echoed the schema example, producing noise rows keyed `mcp-<taskId>-<ts>`
30
30
  // that double-counted alongside the harness's authoritative entry.
31
31
 
32
+ // Deliberately narrow and phrase-based (not a bare "wait"/"block" substring
33
+ // match) to under-fire rather than over-fire: measured against 196 real
34
+ // progress rows across all statuses, this matched 0 — see PR body for the
35
+ // full false-positive measurement methodology.
36
+ const BLOCKED_WAITING_PATTERN =
37
+ /\b(waiting (for|on)|blocked (on|until|by)|still waiting|awaiting)\b/i;
38
+
39
+ // Below this, two check-ins are close enough together that "blocked" reads as
40
+ // noise and calling defer-task buys nothing over checking in again shortly.
41
+ const BLOCKED_WAITING_MIN_ELAPSED_MS = 3 * 60 * 1000;
42
+
32
43
  export const storeProgressOutputSchema = swarmToolOutputSchema({
33
44
  // Bounded confirmation only. The handler keeps the full task row internally
34
45
  // for completion memory, raters, and follow-up creation, but never echoes it
@@ -56,6 +67,12 @@ export const storeProgressOutputSchema = swarmToolOutputSchema({
56
67
  .describe(
57
68
  "True when force: true replaced output and/or failureReason on an already-terminal task without replaying completion side effects.",
58
69
  ),
70
+ blockedWaitingElapsedMs: z
71
+ .number()
72
+ .optional()
73
+ .describe(
74
+ "Present only when this progress text reads as blocked-waiting: milliseconds since the task's prior update. Drives the store-progress nudge toward defer-task.",
75
+ ),
59
76
  });
60
77
 
61
78
  export const registerStoreProgressTool = (server: McpServer) => {
@@ -68,12 +85,19 @@ export const registerStoreProgressTool = (server: McpServer) => {
68
85
  annotations: { idempotentHint: true },
69
86
 
70
87
  inputSchema: z.object({
71
- taskId: z.uuid().describe("The ID of the task to update progress for."),
88
+ taskId: z
89
+ .uuid()
90
+ .optional()
91
+ .describe(
92
+ "Full task UUID. Defaults to the caller-owned task in X-Source-Task-Id; required outside task context.",
93
+ ),
72
94
  progress: z.string().optional().describe("The progress update to store."),
73
95
  status: z
74
- .enum(["completed", "failed"])
96
+ .enum(["completed", "failed", "in_progress", "pending"])
75
97
  .optional()
76
- .describe("Set to 'completed' or 'failed' to finish the task."),
98
+ .describe(
99
+ "Set to 'completed' or 'failed' to finish the task. 'in_progress' and 'pending' store progress only and do not change task status.",
100
+ ),
77
101
  output: z
78
102
  .string()
79
103
  .optional()
@@ -111,7 +135,16 @@ export const registerStoreProgressTool = (server: McpServer) => {
111
135
  outputSchema: storeProgressOutputSchema,
112
136
  },
113
137
  async (
114
- { taskId, progress, status, output, failureReason, attachments, persistMemory, force },
138
+ {
139
+ taskId: requestedTaskId,
140
+ progress,
141
+ status: requestedStatus,
142
+ output,
143
+ failureReason,
144
+ attachments,
145
+ persistMemory,
146
+ force,
147
+ },
115
148
  requestInfo,
116
149
  _meta,
117
150
  ) => {
@@ -119,6 +152,21 @@ export const registerStoreProgressTool = (server: McpServer) => {
119
152
  return toolErr('Agent ID not found. The MCP client should define the "X-Agent-ID" header.');
120
153
  }
121
154
 
155
+ const taskId = requestedTaskId ?? requestInfo.sourceTaskId;
156
+ if (!taskId || !z.uuid().safeParse(taskId).success) {
157
+ return toolErr("Supply taskId as the full task UUID; no valid task context is available.");
158
+ }
159
+ if (!requestedTaskId) {
160
+ const contextTask = await getTaskById(taskId);
161
+ if (!contextTask || contextTask.agentId !== requestInfo.agentId) {
162
+ return toolErr("Omitted taskId requires a source task assigned to the calling agent.");
163
+ }
164
+ }
165
+ const status =
166
+ requestedStatus === "completed" || requestedStatus === "failed"
167
+ ? requestedStatus
168
+ : undefined;
169
+
122
170
  // Verify agent-fs pointers before opening the write transaction. The
123
171
  // registering agent's resolved config selects both credentials and the
124
172
  // exact org/drive; never let the provider fall back to a personal drive.
@@ -220,6 +268,27 @@ export const registerStoreProgressTool = (server: McpServer) => {
220
268
 
221
269
  let updatedTask = existingTask;
222
270
  const isTerminal = isTerminalTaskStatus(existingTask.status);
271
+ // This call's own status can finish the task even though existingTask
272
+ // (its state before this call) is not yet terminal — gate on both so a
273
+ // completing call carrying blocked-waiting-shaped text (e.g. "awaiting
274
+ // review") never nudges toward defer-task.
275
+ const goingTerminal = status !== undefined && isTerminalTaskStatus(status);
276
+
277
+ // Computed against the task's state as of BEFORE this call's update,
278
+ // so "elapsed" reads as time since the prior check-in, not zero.
279
+ let blockedWaitingElapsedMs: number | undefined;
280
+ if (progress && !isTerminal && !goingTerminal && BLOCKED_WAITING_PATTERN.test(progress)) {
281
+ const referenceIso = existingTask.lastUpdatedAt ?? existingTask.createdAt;
282
+ if (referenceIso) {
283
+ const elapsed = Date.now() - new Date(referenceIso).getTime();
284
+ // Below the floor, a sub-minute check-in reads as "blocked" purely
285
+ // from noise, and defer-task buys nothing over just checking in
286
+ // again shortly — so treat it as not blocked-waiting yet.
287
+ if (elapsed >= BLOCKED_WAITING_MIN_ELAPSED_MS) {
288
+ blockedWaitingElapsedMs = elapsed;
289
+ }
290
+ }
291
+ }
223
292
 
224
293
  // Attachments — pointer-based, append-only. Insert each row inside
225
294
  // this transaction; the helper dedups by sha256 (when present) or by
@@ -391,6 +460,7 @@ export const registerStoreProgressTool = (server: McpServer) => {
391
460
  ? `Task "${taskId}" marked as ${status}.`
392
461
  : `Progress stored for task "${taskId}".`,
393
462
  task: updatedTask,
463
+ blockedWaitingElapsedMs,
394
464
  };
395
465
  });
396
466
 
@@ -432,6 +502,10 @@ export const registerStoreProgressTool = (server: McpServer) => {
432
502
  ...("wasForcedOverwrite" in result && result.wasForcedOverwrite
433
503
  ? { wasForcedOverwrite: true }
434
504
  : {}),
505
+ ...("blockedWaitingElapsedMs" in result &&
506
+ typeof result.blockedWaitingElapsedMs === "number"
507
+ ? { blockedWaitingElapsedMs: result.blockedWaitingElapsedMs }
508
+ : {}),
435
509
  };
436
510
  return success ? toolOk(message, { data }) : toolErr(message, { data });
437
511
  },
@@ -197,6 +197,7 @@ Use the crashed agent's identity above as context for who was on it and what kin
197
197
 
198
198
  Dispatch via \`send-task\` with ALL of:
199
199
  - an explicit \`agentId\` (the chosen worker) — REQUIRED. If you omit it, \`send-task\` auto-routes to the original task's agent, which is the dead worker, and the work re-strands.
200
+ - \`routingReason: "reroute_fault"\`
200
201
  - \`taskType: "resume"\`
201
202
  - the tag \`resume-generation:{{generation_next}}\`
202
203
  - \`parentTaskId: {{original_task_id}}\`
@@ -251,7 +252,7 @@ Required capabilities: {{required_capabilities}}{{artifacts_block}}
251
252
 
252
253
  This task has been sitting unassigned because no currently-registered agent matches its required role/capabilities. Pick an agent to take this work over and RE-DELEGATE it — do NOT execute it yourself.
253
254
 
254
- Dispatch via \`send-task\` with an explicit \`agentId\` (REQUIRED — omitting it re-pools the work under the same affinity tag and it will starve again) and \`parentTaskId: {{original_task_id}}\`.
255
+ Dispatch via \`send-task\` with an explicit \`agentId\` (REQUIRED — omitting it re-pools the work under the same affinity tag and it will starve again), \`routingReason: "overflow"\`, and \`parentTaskId: {{original_task_id}}\`.
255
256
 
256
257
  This work will NOT fall back to the unassigned pool — you are the only re-delegation path.`,
257
258
  variables: [