@desplega.ai/agent-swarm 1.131.1 → 1.133.1

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 (250) hide show
  1. package/README.md +2 -2
  2. package/dist/{actions-jgcy477k.js → actions-cskpayd7.js} +11 -9
  3. package/dist/{anthropic-messages-rjmd2e7n.js → anthropic-messages-tpxtbx7b.js} +28 -12
  4. package/dist/{app-5da78bd4.js → app-ang77nbb.js} +8 -6
  5. package/dist/{assistant-f2vkk1hj.js → assistant-b4exebdw.js} +16 -13
  6. package/dist/{azure-openai-responses-c0yjmbfd.js → azure-openai-responses-m3921t4y.js} +8 -8
  7. package/dist/{boot-reembed-q7mp8hcr.js → boot-reembed-ps2aftpb.js} +9 -7
  8. package/dist/{boot-reembed-xmw8wcx6.js → boot-reembed-znyk3nxb.js} +10 -8
  9. package/dist/{boot-scrub-logs-dsqrv79n.js → boot-scrub-logs-2ax3xgz1.js} +7 -5
  10. package/dist/{claude-adapter-q6x7yspx.js → claude-adapter-3ct8kgxw.js} +13 -9
  11. package/dist/{claude-managed-adapter-wsg37bt8.js → claude-managed-adapter-akx4ntf2.js} +2 -2
  12. package/dist/{cli-114fvtmj.js → cli-13w2hwkh.js} +1 -1
  13. package/dist/{cli-4rkfg1wx.js → cli-1532szb1.js} +8 -6
  14. package/dist/{cli-g74vpjp2.js → cli-23d3rseb.js} +33 -17
  15. package/dist/{cli-3ty0t082.js → cli-2a75z9c5.js} +196 -7
  16. package/dist/{cli-pxfxkx5m.js → cli-2t30rj0d.js} +1 -1
  17. package/dist/{cli-rgrfzg8z.js → cli-3c3tdgab.js} +10 -10
  18. package/dist/{cli-6dp3ydxa.js → cli-3zzxa9jz.js} +15 -11
  19. package/dist/{cli-2f07m0xr.js → cli-4k6233gc.js} +1 -1
  20. package/dist/{cli-t8d349cy.js → cli-5p2tz05r.js} +2 -2
  21. package/dist/{cli-w2wxd56v.js → cli-5z2yhzvd.js} +127 -49
  22. package/dist/{cli-kd1ck4rx.js → cli-6zfq31nj.js} +9 -3
  23. package/dist/{cli-er3a4bb0.js → cli-8m9de10a.js} +6 -1
  24. package/dist/{cli-gm9cbhbq.js → cli-8t14vg9w.js} +27 -8
  25. package/dist/{cli-ccf9vy7q.js → cli-9nfc2es8.js} +6 -6
  26. package/dist/{cli-ycrnynpw.js → cli-9ta3pwms.js} +11 -4
  27. package/dist/{cli-7dm2zqy2.js → cli-avr0ns7z.js} +2 -2
  28. package/dist/{cli-n6h8qt8n.js → cli-c897wyhw.js} +12 -7
  29. package/dist/{cli-gecy3z2s.js → cli-dgk20ft7.js} +4 -4
  30. package/dist/cli-et1bbwqf.js +13 -0
  31. package/dist/{cli-bqw0mt1n.js → cli-f5w27pj3.js} +3 -3
  32. package/dist/{cli-2wadx7p2.js → cli-f8dre4bz.js} +2 -2
  33. package/dist/{cli-6pcz9h15.js → cli-ftwpv451.js} +170 -387
  34. package/dist/{cli-nrkkknat.js → cli-g854s2dw.js} +1 -1
  35. package/dist/{cli-793jvbsh.js → cli-ggmdxeqa.js} +13 -8
  36. package/dist/cli-hkwfe3qj.js +530 -0
  37. package/dist/{cli-5dsbcjjw.js → cli-jqwnqx9m.js} +1 -1
  38. package/dist/{cli-9dr5qzc0.js → cli-ny122vh4.js} +21 -10
  39. package/dist/{cli-sy7jk3tx.js → cli-pvzqhbs9.js} +2 -2
  40. package/dist/cli-pzq5rjym.js +15 -0
  41. package/dist/{cli-xpxde7rm.js → cli-qes8ef67.js} +48 -30
  42. package/dist/{cli-sajgpkqg.js → cli-qs9hhsvb.js} +1723 -325
  43. package/dist/cli-qys3rhhz.js +27 -0
  44. package/dist/{cli-se2p6rkb.js → cli-r6tpm37h.js} +2 -2
  45. package/dist/{cli-g1re8r05.js → cli-rch80c76.js} +2 -2
  46. package/dist/{cli-0258203w.js → cli-rpp0x4zy.js} +137 -17
  47. package/dist/{cli-5n6e5yfn.js → cli-s1k0wxhe.js} +12 -7
  48. package/dist/{cli-aca5pz9f.js → cli-sfd9v7sj.js} +1 -1
  49. package/dist/{cli-2rk0cx7s.js → cli-sh3ytsyj.js} +49 -39
  50. package/dist/{cli-zsspvvm2.js → cli-w1mp9jy7.js} +1 -1
  51. package/dist/{cli-6dpytdr2.js → cli-xdb4mpnp.js} +2 -2
  52. package/dist/{cli-57khth04.js → cli-xmyqv00z.js} +120 -4
  53. package/dist/{cli-jbcva0xj.js → cli-xv8t8p3c.js} +56 -31
  54. package/dist/{cli-f1am9szv.js → cli-xxv5pn4f.js} +1 -1
  55. package/dist/{cli-jevgrb4w.js → cli-y08jvxzx.js} +601 -140
  56. package/dist/{cli-50vbt00w.js → cli-zfbf3srj.js} +650 -245
  57. package/dist/{cli-ygrbmjja.js → cli-zmq89k34.js} +6 -6
  58. package/dist/cli.js +22 -22
  59. package/dist/{codex-adapter-htjkn22b.js → codex-adapter-bef93pq4.js} +9 -7
  60. package/dist/{codex-hook-hq54t9ft.js → codex-hook-e8dba65p.js} +3 -2
  61. package/dist/{codex-session-runner-wjfc7smz.js → codex-session-runner-5pkxys7x.js} +9 -7
  62. package/dist/{commands-xhrfwqsk.js → commands-e23s1bff.js} +9 -7
  63. package/dist/{db-anvwb22f.js → db-m3nabatn.js} +17 -5
  64. package/dist/{google-generative-ai-hysrd8qx.js → google-generative-ai-8maw55xh.js} +11 -8
  65. package/dist/{google-vertex-p5arrga0.js → google-vertex-8vjqmx8g.js} +11 -8
  66. package/dist/{handlers-b9kbc621.js → handlers-akazedvr.js} +16 -13
  67. package/dist/{hook-xkvn6c5h.js → hook-ze4cpf0v.js} +55 -41
  68. package/dist/{hook-cgk4wxmx.js → hook-zfqyyenw.js} +10 -7
  69. package/dist/{http-9xh6ta4r.js → http-sja0n6kw.js} +608 -147
  70. package/dist/index-0gr8vd6k.js +39 -0
  71. package/dist/{index-eaxyqcmj.js → index-354vkhdv.js} +154 -74
  72. package/dist/{index-070v4eyb.js → index-5qthek5t.js} +2 -3
  73. package/dist/{index-25wjsdv2.js → index-95t3cqq5.js} +16 -14
  74. package/dist/{index-9znhar3x.js → index-sea66619.js} +3 -4
  75. package/dist/{index-ywds4812.js → index-vpn36mma.js} +15 -13
  76. package/dist/{keepalive-vc7s2aa6.js → keepalive-3sp3meqj.js} +16 -10
  77. package/dist/{lead-r2yqfmx7.js → lead-mefeewkf.js} +39 -36
  78. package/dist/{maintenance-mw70k468.js → maintenance-8tbrbjfg.js} +10 -8
  79. package/dist/mistral-conversations-00zrp7g4.js +717 -0
  80. package/dist/{oauth-refresh-sweep-yecgjpk4.js → oauth-refresh-sweep-srjwjz6m.js} +8 -6
  81. package/dist/{onboard-xj0f73f9.js → onboard-mt2tgyjg.js} +7 -7
  82. package/dist/{openai-codex-responses-5g27pd6f.js → openai-codex-responses-map32h2s.js} +18 -18
  83. package/dist/{openai-completions-3vkmfz2a.js → openai-completions-w871b1cq.js} +12 -11
  84. package/dist/{openai-responses-dg4t7j7a.js → openai-responses-b0j7xnw8.js} +15 -12
  85. package/dist/{opencode-adapter-1gm38aba.js → opencode-adapter-b39jefcj.js} +5 -3
  86. package/dist/{openrouter-images-g3gm8931.js → openrouter-images-srfkywy0.js} +4 -4
  87. package/dist/{otel-impl-nxg0n0b3.js → otel-impl-z6pxt3j9.js} +224 -9
  88. package/dist/{pi-mono-adapter-4g5kby31.js → pi-mono-adapter-btvqmj58.js} +1138 -408
  89. package/dist/{pricing-refresh-2cn6effr.js → pricing-refresh-kyq8cezh.js} +9 -7
  90. package/dist/{rbac-roles-1hcxdtg7.js → rbac-roles-3ryfrfna.js} +8 -6
  91. package/dist/{rbac-roles-vynday5g.js → rbac-roles-6qfvbee1.js} +9 -7
  92. package/dist/{render-v2-tc0bh8kk.js → render-v2-p61ppjey.js} +8 -6
  93. package/dist/{seed-pricing-8kceb43f.js → seed-pricing-j56bbj4y.js} +8 -6
  94. package/dist/{setup-46xtgq3c.js → setup-tpr6c1kn.js} +2 -2
  95. package/dist/{worker-qrbz6hmv.js → worker-9w1v0mbb.js} +39 -36
  96. package/openapi.json +542 -16
  97. package/package.json +6 -6
  98. package/src/be/budget-refusal-notify.ts +14 -7
  99. package/src/be/db-queries/task-artifacts.ts +182 -0
  100. package/src/be/db.ts +783 -169
  101. package/src/be/events.ts +5 -0
  102. package/src/be/migrations/131_session_logs_created_at_index.sql +2 -0
  103. package/src/be/migrations/132_multi_runtime_instances.sql +44 -0
  104. package/src/be/migrations/133_workflow_run_trigger_type.sql +12 -0
  105. package/src/be/migrations/135_backfill_task_pull_request_attachments.sql +228 -0
  106. package/src/be/migrations/136_task_requester_provenance.sql +37 -0
  107. package/src/be/migrations/137_memory_retrieval_composite_index.sql +1 -0
  108. package/src/be/multi-runtime.ts +430 -0
  109. package/src/be/scripts/extract-schema.ts +3 -1
  110. package/src/be/scripts/typecheck.ts +3 -2
  111. package/src/be/seed-skills/index.ts +7 -0
  112. package/src/be/steering.ts +1 -1
  113. package/src/be/swarm-config-guard.ts +18 -0
  114. package/src/commands/credential-wait.ts +54 -0
  115. package/src/commands/onboard/steps/core-credentials.tsx +1 -1
  116. package/src/commands/onboard/steps/custom-templates.tsx +1 -1
  117. package/src/commands/onboard/steps/harness-credentials.tsx +1 -1
  118. package/src/commands/onboard/steps/health-check.tsx +1 -1
  119. package/src/commands/onboard/steps/start.tsx +1 -1
  120. package/src/commands/profile-sync-audit.ts +56 -0
  121. package/src/commands/profile-sync.ts +287 -15
  122. package/src/commands/provider-credentials.ts +46 -19
  123. package/src/commands/runner.ts +196 -33
  124. package/src/commands/templates.ts +22 -0
  125. package/src/github/handlers.ts +55 -9
  126. package/src/heartbeat/heartbeat.ts +71 -12
  127. package/src/hooks/hook.ts +49 -33
  128. package/src/http/active-sessions.ts +11 -1
  129. package/src/http/agents.ts +261 -30
  130. package/src/http/all-routes.ts +1 -0
  131. package/src/http/config.ts +13 -2
  132. package/src/http/core.ts +102 -4
  133. package/src/http/db-query-bounded.ts +313 -0
  134. package/src/http/db-query-shared.ts +249 -0
  135. package/src/http/db-query.ts +54 -42
  136. package/src/http/events.ts +2 -0
  137. package/src/http/index.ts +22 -2
  138. package/src/http/mcp-bridge.ts +37 -3
  139. package/src/http/mcp.ts +12 -0
  140. package/src/http/memory.ts +13 -2
  141. package/src/http/openapi.ts +1 -0
  142. package/src/http/poll.ts +70 -27
  143. package/src/http/route-def.ts +43 -9
  144. package/src/http/schedules.ts +2 -2
  145. package/src/http/scripts.ts +10 -1
  146. package/src/http/session-data.ts +44 -0
  147. package/src/http/tasks.ts +2 -2
  148. package/src/http/users.ts +74 -52
  149. package/src/http/webhooks.ts +5 -2
  150. package/src/http/workflows.ts +1 -4
  151. package/src/integrations/kapso/inbound.ts +1 -1
  152. package/src/jira/outbound.ts +39 -8
  153. package/src/jira/webhook-lifecycle.ts +13 -2
  154. package/src/linear/outbound.ts +43 -10
  155. package/src/oauth/keepalive.ts +13 -2
  156. package/src/prompts/base-prompt.ts +1 -1
  157. package/src/prompts/session-templates.ts +45 -7
  158. package/src/providers/claude-adapter.ts +6 -0
  159. package/src/providers/codex-adapter.ts +6 -0
  160. package/src/providers/opencode-adapter.ts +6 -0
  161. package/src/providers/pi-mono-adapter.ts +8 -0
  162. package/src/providers/pricing-sources.md +24 -0
  163. package/src/queue-stall-alarm.ts +216 -0
  164. package/src/scheduler/scheduler.ts +7 -0
  165. package/src/script-workflows/executor.ts +2 -1
  166. package/src/scripts-runtime/executors/native.ts +2 -1
  167. package/src/scripts-runtime/executors/types.ts +5 -0
  168. package/src/scripts-runtime/loader.ts +5 -0
  169. package/src/scripts-runtime/swarm-config.ts +8 -0
  170. package/src/scripts-runtime/swarm-sdk.ts +7 -0
  171. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  172. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  173. package/src/tests/budget-claim-gate.test.ts +20 -0
  174. package/src/tests/claude-adapter.test.ts +32 -0
  175. package/src/tests/codex-adapter.test.ts +30 -0
  176. package/src/tests/create-task-validation.test.ts +128 -0
  177. package/src/tests/credential-wait.test.ts +67 -1
  178. package/src/tests/db-query.test.ts +711 -2
  179. package/src/tests/http-api-integration.test.ts +81 -0
  180. package/src/tests/http-users.test.ts +166 -1
  181. package/src/tests/identity-field-budget.test.ts +176 -0
  182. package/src/tests/mcp-tools-user.test.ts +34 -0
  183. package/src/tests/mcp-user-route.test.ts +17 -0
  184. package/src/tests/metrics-http.test.ts +44 -0
  185. package/src/tests/multi-runtime-registration.test.ts +2703 -0
  186. package/src/tests/openapi-response-contract.test.ts +24 -0
  187. package/src/tests/openapi-route-headers.test.ts +83 -0
  188. package/src/tests/opencode-adapter.test.ts +40 -0
  189. package/src/tests/outbound-listener-rejection.test.ts +138 -0
  190. package/src/tests/pi-mono-adapter.test.ts +98 -0
  191. package/src/tests/profile-sync-warning.test.ts +277 -0
  192. package/src/tests/profile-sync.test.ts +232 -6
  193. package/src/tests/prompt-template-session.test.ts +32 -3
  194. package/src/tests/queue-stall-alarm.test.ts +226 -0
  195. package/src/tests/requester-comms.test.ts +32 -0
  196. package/src/tests/runner-requester-profile.test.ts +38 -0
  197. package/src/tests/schedule-target-type.test.ts +38 -0
  198. package/src/tests/script-connections.test.ts +13 -1
  199. package/src/tests/scripts-runtime-identity.test.ts +375 -0
  200. package/src/tests/scripts-runtime.test.ts +4 -3
  201. package/src/tests/sdk-allowlist.test.ts +47 -0
  202. package/src/tests/send-task-requested-by.test.ts +9 -0
  203. package/src/tests/session-costs.test.ts +506 -1
  204. package/src/tests/shutdown-error-scrubbing.test.ts +92 -0
  205. package/src/tests/steering-core.test.ts +28 -0
  206. package/src/tests/task-artifacts.test.ts +386 -0
  207. package/src/tests/task-attachments-schema.test.ts +17 -1
  208. package/src/tests/task-completion-idempotency.test.ts +57 -0
  209. package/src/tests/task-pull-request-backfill-migration.test.ts +321 -0
  210. package/src/tests/update-profile-api.test.ts +67 -39
  211. package/src/tests/update-profile-auth.test.ts +48 -4
  212. package/src/tests/user-attribution-migration.test.ts +54 -0
  213. package/src/tests/user-identity.test.ts +72 -0
  214. package/src/tests/workflow-async-v2.test.ts +39 -1
  215. package/src/tests/workflow-http-v2.test.ts +3 -2
  216. package/src/tests/workflow-mcp-trigger-schema.test.ts +52 -3
  217. package/src/tests/workflow-retry-v2.test.ts +45 -1
  218. package/src/tools/db-query.ts +18 -8
  219. package/src/tools/get-task-details.ts +24 -3
  220. package/src/tools/manage-user.ts +36 -3
  221. package/src/tools/poll-task.ts +93 -14
  222. package/src/tools/script-common.ts +5 -0
  223. package/src/tools/send-task.ts +1 -4
  224. package/src/tools/store-progress.ts +13 -2
  225. package/src/tools/swarm-config/delete-config.ts +9 -0
  226. package/src/tools/swarm-config/set-config.ts +3 -2
  227. package/src/tools/task-action.ts +30 -0
  228. package/src/tools/task-tool-ctx.ts +8 -1
  229. package/src/tools/update-profile.ts +16 -21
  230. package/src/tools/utils.ts +5 -0
  231. package/src/tools/workflows/trigger-workflow.ts +7 -1
  232. package/src/types.ts +165 -1
  233. package/src/utils/github-pull-request.ts +76 -0
  234. package/src/utils/identity-field-budget.ts +73 -0
  235. package/src/utils/multi-runtime.ts +20 -0
  236. package/src/utils/requester-comms.ts +24 -0
  237. package/src/workflows/engine.ts +22 -2
  238. package/src/workflows/resume.ts +19 -11
  239. package/src/workflows/retry-poller.ts +1 -0
  240. package/src/workflows/triggers.ts +4 -0
  241. package/templates/skills/db-query-guidance/config.json +14 -0
  242. package/templates/skills/db-query-guidance/content.md +57 -0
  243. package/dist/cli-3te4jdkm.js +0 -225
  244. package/dist/cli-bmze986d.js +0 -7
  245. package/dist/cli-kh3ba2zf.js +0 -607
  246. package/dist/index-vg4snay4.js +0 -36
  247. package/dist/mistral-conversations-81fwav0j.js +0 -30578
  248. package/dist/otel-nqa12wf3.js +0 -3149
  249. package/dist/{cli-wz9115x0.js → cli-zr9q011v.js} +6 -6
  250. package/dist/{pi-messages-tqj8q6b9.js → pi-messages-sssy32x4.js} +3 -3
@@ -3,15 +3,21 @@ import { unlinkSync } from "node:fs";
3
3
  import {
4
4
  closeDb,
5
5
  createAgent,
6
+ createScheduledTask,
6
7
  createTaskExtended,
7
8
  createUser,
9
+ createWorkflow,
10
+ createWorkflowRun,
8
11
  deleteUser,
9
12
  getAllUsers,
10
13
  getDb,
14
+ getScheduledTaskById,
11
15
  getTaskById,
12
16
  getUserById,
17
+ getWorkflowRun,
13
18
  initDb,
14
19
  updateUser,
20
+ updateWorkflowRun,
15
21
  } from "../be/db";
16
22
  import {
17
23
  findOrCreateUserByEmail,
@@ -276,13 +282,69 @@ describe("deleteUser", () => {
276
282
  source: "slack",
277
283
  requestedByUserId: user.id,
278
284
  });
285
+ getDb().prepare("UPDATE agent_tasks SET created_by = ? WHERE id = ?").run(user.id, task.id);
279
286
  expect(getTaskById(task.id)!.requestedByUserId).toBe(user.id);
287
+ expect(
288
+ getDb()
289
+ .prepare<{ created_by: string | null }, [string]>(
290
+ "SELECT created_by FROM agent_tasks WHERE id = ?",
291
+ )
292
+ .get(task.id)?.created_by,
293
+ ).toBe(user.id);
280
294
 
281
295
  deleteUser(user.id);
282
296
  expect(getTaskById(task.id)!.requestedByUserId).toBeUndefined();
297
+ expect(
298
+ getDb()
299
+ .prepare<{ created_by: string | null }, [string]>(
300
+ "SELECT created_by FROM agent_tasks WHERE id = ?",
301
+ )
302
+ .get(task.id)?.created_by,
303
+ ).toBeNull();
283
304
  // ON DELETE CASCADE on user_external_ids.userId should clear the mapping.
284
305
  expect(findUserByExternalId("slack", "U_TASKOWNER")).toBeNull();
285
306
  });
307
+
308
+ test("clears retained workflow run attribution before deleting the user", () => {
309
+ const user = createUser({ name: "Workflow Trigger" });
310
+ const workflow = createWorkflow({
311
+ name: `delete-user-workflow-${crypto.randomUUID()}`,
312
+ definition: { nodes: [] },
313
+ });
314
+ const run = createWorkflowRun({
315
+ id: crypto.randomUUID(),
316
+ workflowId: workflow.id,
317
+ createdBy: user.id,
318
+ });
319
+ updateWorkflowRun(run.id, {
320
+ context: {
321
+ swarm: { requestedByUserId: user.id, retained: "swarm-value" },
322
+ retained: "root-value",
323
+ },
324
+ });
325
+
326
+ expect(deleteUser(user.id)).toBe(true);
327
+ expect(getUserById(user.id)).toBeNull();
328
+ expect(getWorkflowRun(run.id)?.createdBy).toBeUndefined();
329
+ expect(getWorkflowRun(run.id)?.context).toEqual({
330
+ swarm: { retained: "swarm-value" },
331
+ retained: "root-value",
332
+ });
333
+ });
334
+
335
+ test("clears schedule attribution whose audit FKs were dropped by migration 103", () => {
336
+ const user = createUser({ name: "Schedule Trigger" });
337
+ const schedule = createScheduledTask({
338
+ name: `delete-user-schedule-${crypto.randomUUID()}`,
339
+ intervalMs: 60_000,
340
+ taskTemplate: "Run scheduled work",
341
+ createdBy: user.id,
342
+ });
343
+
344
+ expect(deleteUser(user.id)).toBe(true);
345
+ expect(getScheduledTaskById(schedule.id)?.createdBy).toBeUndefined();
346
+ expect(getScheduledTaskById(schedule.id)?.updatedBy).toBeUndefined();
347
+ });
286
348
  });
287
349
 
288
350
  // ─── findUserByExternalId ─────────────────────────────────────────────────────
@@ -518,6 +580,13 @@ describe("recordIdentityEvent", () => {
518
580
  // ─── requestedByUserId on tasks ───────────────────────────────────────────────
519
581
 
520
582
  describe("requestedByUserId in tasks", () => {
583
+ const requesterWasInherited = (taskId: string): number | undefined =>
584
+ getDb()
585
+ .prepare<{ requestedByUserIdInherited: number }, [string]>(
586
+ "SELECT requestedByUserIdInherited FROM agent_tasks WHERE id = ?",
587
+ )
588
+ .get(taskId)?.requestedByUserIdInherited;
589
+
521
590
  test("createTaskExtended stores requestedByUserId", () => {
522
591
  const user = createUser({ name: "Requester" });
523
592
  const task = createTaskExtended("task with requester", {
@@ -527,6 +596,7 @@ describe("requestedByUserId in tasks", () => {
527
596
  });
528
597
  const fetched = getTaskById(task.id);
529
598
  expect(fetched!.requestedByUserId).toBe(user.id);
599
+ expect(requesterWasInherited(task.id)).toBe(0);
530
600
  deleteUser(user.id);
531
601
  });
532
602
 
@@ -544,6 +614,7 @@ describe("requestedByUserId in tasks", () => {
544
614
  });
545
615
  const fetchedChild = getTaskById(child.id);
546
616
  expect(fetchedChild!.requestedByUserId).toBe(user.id);
617
+ expect(requesterWasInherited(child.id)).toBe(1);
547
618
  deleteUser(user.id);
548
619
  });
549
620
 
@@ -562,6 +633,7 @@ describe("requestedByUserId in tasks", () => {
562
633
  requestedByUserId: user2.id,
563
634
  });
564
635
  expect(getTaskById(child.id)!.requestedByUserId).toBe(user2.id);
636
+ expect(requesterWasInherited(child.id)).toBe(0);
565
637
  deleteUser(user1.id);
566
638
  deleteUser(user2.id);
567
639
  });
@@ -152,7 +152,7 @@ describe("Workflow Async v2 (Phase 4)", () => {
152
152
  expect(task!.task).toBe("Do the thing");
153
153
  });
154
154
 
155
- test("inherits requestedByUserId from workflow execution options", async () => {
155
+ test("persists the requester on the run and attributes the root task", async () => {
156
156
  const user = createUser({ name: "Workflow Requester" });
157
157
  const workflow = makeWorkflow({
158
158
  nodes: [
@@ -170,6 +170,7 @@ describe("Workflow Async v2 (Phase 4)", () => {
170
170
  const steps = getWorkflowRunStepsByRunId(runId);
171
171
  const task = getTaskByWorkflowRunStepId(steps[0]!.id);
172
172
 
173
+ expect(getWorkflowRun(runId)?.createdBy).toBe(user.id);
173
174
  expect(task).toBeTruthy();
174
175
  expect(task!.requestedByUserId).toBe(user.id);
175
176
  });
@@ -251,6 +252,43 @@ describe("Workflow Async v2 (Phase 4)", () => {
251
252
  expect(completedSteps).toHaveLength(2);
252
253
  });
253
254
 
255
+ test("resume rehydrates requester attribution from the workflow run", async () => {
256
+ const user = createUser({ name: "Resumed Workflow Requester" });
257
+ const workflow = makeWorkflow({
258
+ nodes: [
259
+ {
260
+ id: "task1",
261
+ type: "agent-task",
262
+ config: { template: "First attributed task" },
263
+ next: "task2",
264
+ },
265
+ {
266
+ id: "task2",
267
+ type: "agent-task",
268
+ config: { template: "Second attributed task" },
269
+ },
270
+ ],
271
+ });
272
+
273
+ const runId = await startWorkflowExecution(workflow, {}, registry, {
274
+ requestedByUserId: user.id,
275
+ });
276
+ const firstStep = getWorkflowRunStepsByRunId(runId).find((step) => step.nodeId === "task1")!;
277
+ const firstTask = getTaskByWorkflowRunStepId(firstStep.id)!;
278
+
279
+ workflowEventBus.emit("task.completed", {
280
+ taskId: firstTask.id,
281
+ output: "done",
282
+ workflowRunId: runId,
283
+ workflowRunStepId: firstStep.id,
284
+ });
285
+ await new Promise((resolve) => setTimeout(resolve, 10));
286
+
287
+ const secondStep = getWorkflowRunStepsByRunId(runId).find((step) => step.nodeId === "task2");
288
+ expect(secondStep).toBeDefined();
289
+ expect(getTaskByWorkflowRunStepId(secondStep!.id)?.requestedByUserId).toBe(user.id);
290
+ });
291
+
254
292
  test("resume from task failure marks run as failed", async () => {
255
293
  const workflow = makeWorkflow({
256
294
  nodes: [
@@ -607,7 +607,7 @@ describe("Workflow HTTP API v2", () => {
607
607
  expect(res.status).toBe(201);
608
608
  });
609
609
 
610
- test("falls back to the workflow author when the trigger has no user auth", async () => {
610
+ test("does not attribute a trigger with no user auth to the workflow author", async () => {
611
611
  const author = createUser({
612
612
  name: "Workflow Author",
613
613
  email: `workflow-author-${crypto.randomUUID()}@example.com`,
@@ -622,7 +622,8 @@ describe("Workflow HTTP API v2", () => {
622
622
  });
623
623
  expect(res.status).toBe(201);
624
624
  const { runId } = (await res.json()) as { runId: string };
625
- expect(getWorkflowRun(runId)?.context?.swarm).toEqual({ requestedByUserId: author.id });
625
+ expect(getWorkflowRun(runId)?.createdBy).toBeUndefined();
626
+ expect(getWorkflowRun(runId)?.context?.swarm).toBeUndefined();
626
627
  });
627
628
 
628
629
  test("returns 400 for disabled workflow", async () => {
@@ -8,7 +8,19 @@ import {
8
8
  type ServerResponse,
9
9
  } from "node:http";
10
10
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
- import { closeDb, deleteWorkflow, getWorkflow, initDb } from "../be/db";
11
+ import {
12
+ closeDb,
13
+ createAgent,
14
+ createTaskExtended,
15
+ createUser,
16
+ createWorkflow,
17
+ deleteWorkflow,
18
+ getTaskByWorkflowRunStepId,
19
+ getWorkflow,
20
+ getWorkflowRun,
21
+ getWorkflowRunStepsByRunId,
22
+ initDb,
23
+ } from "../be/db";
12
24
  import { getPathSegments, parseQueryParams } from "../http/utils";
13
25
  import { handleWorkflows } from "../http/workflows";
14
26
  import { SCRIPT_LONG_TIMEOUT_HINT_MS } from "../scripts-runtime/executors/types";
@@ -66,12 +78,17 @@ function buildServerWithTools() {
66
78
 
67
79
  const callTool =
68
80
  (name: string) =>
69
- async (args: Record<string, unknown>, agentId = "agent-test") => {
81
+ async (args: Record<string, unknown>, agentId = "agent-test", sourceTaskId?: string) => {
70
82
  const tool = registeredTools[name];
71
83
  expect(tool).toBeDefined();
72
84
  const extra = {
73
85
  sessionId: "session-test",
74
- requestInfo: { headers: { "x-agent-id": agentId } },
86
+ requestInfo: {
87
+ headers: {
88
+ "x-agent-id": agentId,
89
+ ...(sourceTaskId ? { "x-source-task-id": sourceTaskId } : {}),
90
+ },
91
+ },
75
92
  };
76
93
  return (await tool.handler(args, extra)) as ToolResult;
77
94
  };
@@ -579,6 +596,38 @@ describe("MCP create-workflow / update-workflow / patch-workflow accept triggerS
579
596
 
580
597
  // ─── Phase 3: trigger-workflow surfaces TriggerSchemaError ──
581
598
 
599
+ test("trigger-workflow attributes the run and its root task to the calling task's requester", async () => {
600
+ const requester = createUser({ name: "MCP Workflow Requester" });
601
+ const agent = createAgent({
602
+ name: uniqueName("mcp-trigger-agent"),
603
+ isLead: false,
604
+ status: "idle",
605
+ });
606
+ const sourceTask = createTaskExtended("Trigger a workflow", {
607
+ agentId: agent.id,
608
+ requestedByUserId: requester.id,
609
+ });
610
+ const workflow = createWorkflow({
611
+ name: uniqueName("mcp-trigger-attribution"),
612
+ definition: minimalDefinition,
613
+ });
614
+ createdWorkflowIds.push(workflow.id);
615
+
616
+ const triggered = await tools.callTrigger(
617
+ { id: workflow.id, triggerData: { source: "test" } },
618
+ agent.id,
619
+ sourceTask.id,
620
+ );
621
+
622
+ expect(triggered.structuredContent?.success).toBe(true);
623
+ const runId = triggered.structuredContent?.runId as string;
624
+ expect(getWorkflowRun(runId)?.createdBy).toBe(requester.id);
625
+
626
+ const [step] = getWorkflowRunStepsByRunId(runId);
627
+ expect(step).toBeDefined();
628
+ expect(getTaskByWorkflowRunStepId(step!.id)?.requestedByUserId).toBe(requester.id);
629
+ });
630
+
582
631
  test("trigger-workflow with missing required field → structured TriggerSchemaError", async () => {
583
632
  const triggerSchema: Record<string, unknown> = {
584
633
  type: "object",
@@ -3,6 +3,7 @@ import { unlink } from "node:fs/promises";
3
3
  import { z } from "zod";
4
4
  import {
5
5
  closeDb,
6
+ createUser,
6
7
  createWorkflow,
7
8
  createWorkflowRun,
8
9
  createWorkflowRunStep,
@@ -15,7 +16,7 @@ import {
15
16
  updateWorkflowRun,
16
17
  updateWorkflowRunStep,
17
18
  } from "../be/db";
18
- import type { RetryPolicy, Workflow, WorkflowDefinition } from "../types";
19
+ import type { ExecutorMeta, RetryPolicy, Workflow, WorkflowDefinition } from "../types";
19
20
  import { startWorkflowExecution } from "../workflows/engine";
20
21
  import { workflowEventBus } from "../workflows/event-bus";
21
22
  import {
@@ -33,6 +34,7 @@ const TEST_DB_PATH = "./test-workflow-retry-v2.sqlite";
33
34
  // ─── Test Executors ──────────────────────────────────────────
34
35
 
35
36
  let failCounter = 0;
37
+ const failExecutorMeta: ExecutorMeta[] = [];
36
38
 
37
39
  class FailOnceExecutor extends BaseExecutor<
38
40
  typeof FailOnceExecutor.schema,
@@ -48,7 +50,10 @@ class FailOnceExecutor extends BaseExecutor<
48
50
 
49
51
  protected async execute(
50
52
  config: z.infer<typeof FailOnceExecutor.schema>,
53
+ _context: Readonly<Record<string, unknown>>,
54
+ meta: ExecutorMeta,
51
55
  ): Promise<ExecutorResult<z.infer<typeof FailOnceExecutor.outSchema>>> {
56
+ failExecutorMeta.push(meta);
52
57
  failCounter++;
53
58
  if (failCounter <= config.failUntilAttempt) {
54
59
  return { status: "failed", error: `Intentional failure #${failCounter}` };
@@ -219,6 +224,45 @@ describe("Workflow Retry v2 (Phase 4)", () => {
219
224
  });
220
225
 
221
226
  describe("Retry Poller", () => {
227
+ test("rehydrates requester attribution before retrying an executor", async () => {
228
+ const user = createUser({ name: "Retry Requester" });
229
+ const workflow = makeWorkflow({
230
+ nodes: [
231
+ {
232
+ id: "attributed-retry",
233
+ type: "fail-once",
234
+ config: { failUntilAttempt: 1 },
235
+ retry: {
236
+ strategy: "static",
237
+ maxRetries: 2,
238
+ baseDelayMs: 10,
239
+ maxDelayMs: 10,
240
+ },
241
+ },
242
+ ],
243
+ });
244
+
245
+ failCounter = 0;
246
+ failExecutorMeta.length = 0;
247
+ const runId = await startWorkflowExecution(workflow, {}, registry, {
248
+ requestedByUserId: user.id,
249
+ });
250
+ await new Promise((resolve) => setTimeout(resolve, 10));
251
+
252
+ try {
253
+ startRetryPoller(registry, 10);
254
+ for (let i = 0; i < 20 && getWorkflowRun(runId)?.status !== "completed"; i++) {
255
+ await new Promise((resolve) => setTimeout(resolve, 10));
256
+ }
257
+ } finally {
258
+ stopRetryPoller();
259
+ }
260
+
261
+ expect(getWorkflowRun(runId)?.status).toBe("completed");
262
+ expect(failExecutorMeta).toHaveLength(2);
263
+ expect(failExecutorMeta[1]?.requestedByUserId).toBe(user.id);
264
+ });
265
+
222
266
  test("poller picks up failed steps past nextRetryAt", async () => {
223
267
  // Reset fail counter so the executor succeeds on the "retry" attempt
224
268
  failCounter = 0;
@@ -1,14 +1,18 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import * as z from "zod";
3
- import { DbQueryInputShape, executeReadOnlyQuery, resolveDbQuerySql } from "@/http/db-query";
3
+ import { DbQueryInputShape, executeReadOnlyQueryGated, resolveDbQuerySql } from "@/http/db-query";
4
+ import { getDbQueryMcpBudgetMs, getDbQueryMcpMaxRows } from "@/http/db-query-shared";
4
5
  import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
5
6
 
6
- const MCP_MAX_ROWS = 100;
7
-
8
7
  const DbQueryToolInputSchema = z
9
8
  .object({
10
9
  ...DbQueryInputShape,
11
- sql: z.string().optional().describe("SQL query (read-only only — writes are rejected)"),
10
+ sql: z
11
+ .string()
12
+ .optional()
13
+ .describe(
14
+ "Read-only SQL query (writes are rejected). Runs with a wall-clock budget in a bounded child process by default, and results are capped at a default row count (operator-configurable via `DB_QUERY_MCP_MAX_ROWS`) — a query is safe to try even against a huge table. session_logs, agent_log, events, and task_context_snapshots are too large to read whole: filter on an indexed column (session_logs: taskId/sessionId; agent_log: agentId/taskId/eventType/createdAt) and add a LIMIT, don't COUNT(*)/SUM(...)/typeof() across the table, and don't split a large read into rowid chunks — each chunk still reads every row in its range. See the db-query-guidance skill for the operator config knobs (timeout, row cap, concurrency cap) if a query keeps timing out or getting rejected.",
15
+ ),
12
16
  query: z.string().optional().describe("Deprecated runtime alias for sql."),
13
17
  params: z.array(z.any()).optional().default([]).describe("Query parameters"),
14
18
  })
@@ -22,7 +26,7 @@ export const registerDbQueryTool = (server: McpServer) => {
22
26
  {
23
27
  title: "Execute database query",
24
28
  description:
25
- "Execute a read-only SQL query against the swarm database. Available to all authenticated agents — be aware results may include secrets (oauth_tokens, configs). Results capped at 100 rows.",
29
+ "Execute a read-only SQL query against the swarm database (SQLite). Available to all authenticated agents — be aware results may include secrets (oauth_tokens, configs). Runs in a short-lived child process with a wall-clock budget by default (fails gracefully with a timeout or a 429-style concurrency error rather than freezing); results capped at a default row count (operator-configurable via `DB_QUERY_MCP_MAX_ROWS`) regardless of how many the query matched. See the sql parameter's description for which tables are unsafe to read whole, and the db-query-guidance skill for config knobs.",
26
30
  annotations: { readOnlyHint: true },
27
31
  inputSchema: DbQueryToolInputSchema,
28
32
  outputSchema: swarmToolOutputSchema({
@@ -37,15 +41,21 @@ export const registerDbQueryTool = (server: McpServer) => {
37
41
  try {
38
42
  const sql = resolveDbQuerySql(input);
39
43
  const params = input.params ?? [];
40
- const result = executeReadOnlyQuery(sql, params, MCP_MAX_ROWS);
41
- const truncated = result.total > MCP_MAX_ROWS;
44
+ const maxRows = getDbQueryMcpMaxRows();
45
+ const result = await executeReadOnlyQueryGated(
46
+ sql,
47
+ params,
48
+ getDbQueryMcpBudgetMs(),
49
+ maxRows,
50
+ );
51
+ const truncated = result.total > maxRows;
42
52
 
43
53
  // Build a simple text table for Claude
44
54
  const header = result.columns.join(" | ");
45
55
  const separator = result.columns.map(() => "---").join(" | ");
46
56
  const dataRows = result.rows.map((row) => row.map((v) => String(v ?? "NULL")).join(" | "));
47
57
  const table = [header, separator, ...dataRows].join("\n");
48
- const suffix = truncated ? `\n(Showing ${MCP_MAX_ROWS} of ${result.total} rows)` : "";
58
+ const suffix = truncated ? `\n(Showing ${maxRows} of ${result.total} rows)` : "";
49
59
  const details = `${table}${suffix}\n\n${result.total} rows in ${result.elapsed}ms`;
50
60
 
51
61
  return toolOk(`${result.total} row(s) in ${result.elapsed}ms`, {
@@ -15,6 +15,7 @@ import {
15
15
  toolOk,
16
16
  } from "@/tools/utils";
17
17
  import { AgentTaskStatusSchema } from "@/types";
18
+ import { getUserCommsPrefs } from "@/utils/requester-comms";
18
19
 
19
20
  export const getTaskDetailsInputSchema = z.object({
20
21
  taskId: z.uuid().describe("The ID of the task to get details for."),
@@ -157,9 +158,23 @@ export const getTaskDetailsOutputSchema = swarmToolOutputSchema({
157
158
  yourAgentId: z.string().optional(),
158
159
  task: looseAgentTaskOutputSchema.optional(),
159
160
  requestedBy: z
160
- .looseObject({ name: z.string().optional(), email: z.string().optional() })
161
+ .looseObject({
162
+ name: z.string().optional(),
163
+ email: z.string().optional(),
164
+ role: z.string().optional(),
165
+ notes: z.string().optional(),
166
+ comms: z
167
+ .looseObject({
168
+ tone: z.string().optional(),
169
+ language: z.string().optional(),
170
+ verbosity: z.string().optional(),
171
+ })
172
+ .optional(),
173
+ })
161
174
  .optional()
162
- .describe("Resolved user who requested this task"),
175
+ .describe(
176
+ "Resolved user who requested this task, with role/notes and structured communication preferences (users.metadata.comms) when set",
177
+ ),
163
178
  logs: z.array(looseAgentLogSchema).optional(),
164
179
  attachments: z
165
180
  .array(looseTaskAttachmentSchema)
@@ -191,7 +206,13 @@ export async function getTaskDetailsHandler(
191
206
  // Resolve requesting user details if available
192
207
  const requestedByUser = task.requestedByUserId ? getUserById(task.requestedByUserId) : undefined;
193
208
  const requestedBy = requestedByUser
194
- ? { name: requestedByUser.name, email: requestedByUser.email }
209
+ ? {
210
+ name: requestedByUser.name,
211
+ email: requestedByUser.email,
212
+ role: requestedByUser.role,
213
+ notes: requestedByUser.notes,
214
+ comms: getUserCommsPrefs(requestedByUser),
215
+ }
195
216
  : undefined;
196
217
 
197
218
  const data = {
@@ -82,6 +82,17 @@ const InputSchema = z.object({
82
82
  .nullable()
83
83
  .optional()
84
84
  .describe("Free-form JSON metadata (null clears the field)"),
85
+ comms: z
86
+ .object({
87
+ tone: z.string().optional(),
88
+ language: z.string().optional(),
89
+ verbosity: z.string().optional(),
90
+ })
91
+ .nullable()
92
+ .optional()
93
+ .describe(
94
+ "Merge-safe write to metadata.comms without touching sibling metadata keys; null removes only the comms key. When metadata is also provided, it is applied first and replaces the whole blob.",
95
+ ),
85
96
  });
86
97
 
87
98
  function diffAliases(prev: string[], next: string[]): { added: string[]; removed: string[] } {
@@ -99,7 +110,7 @@ export const registerManageUserTool = (server: McpServer) => {
99
110
  {
100
111
  title: "Manage user profiles",
101
112
  description:
102
- "Create, update, delete, or list user profiles in the user registry. Identities are managed via an `identities: [{kind, externalId}]` array (declarative — update computes diff). Lead-only.",
113
+ "Create, update, delete, or list user profiles in the user registry. Identities are managed via an `identities: [{kind, externalId}]` array (declarative — update computes diff). comms is the merge-safe way to set communication preferences; metadata replaces the whole blob. Lead-only.",
103
114
  annotations: { readOnlyHint: false },
104
115
  inputSchema: InputSchema,
105
116
  outputSchema: swarmToolOutputSchema({
@@ -157,6 +168,10 @@ export const registerManageUserTool = (server: McpServer) => {
157
168
  return toolErr("name is required for create action.");
158
169
  }
159
170
  try {
171
+ const metadata =
172
+ input.comms != null
173
+ ? { ...(input.metadata ?? {}), comms: input.comms }
174
+ : (input.metadata ?? undefined);
160
175
  const user = createUser({
161
176
  name: input.name,
162
177
  email: input.email,
@@ -167,7 +182,7 @@ export const registerManageUserTool = (server: McpServer) => {
167
182
  timezone: input.timezone,
168
183
  dailyBudgetUsd: input.dailyBudgetUsd ?? undefined,
169
184
  status: input.status,
170
- metadata: input.metadata ?? undefined,
185
+ metadata,
171
186
  });
172
187
  for (const ident of input.identities ?? []) {
173
188
  linkIdentity(user.id, ident.kind, ident.externalId, operatorActor);
@@ -192,6 +207,24 @@ export const registerManageUserTool = (server: McpServer) => {
192
207
  return toolErr(`User ${input.userId} not found.`);
193
208
  }
194
209
 
210
+ // Merge-safe write: metadata (if provided) is applied first and
211
+ // replaces the whole blob, then comms is merged into it so
212
+ // sibling metadata keys survive.
213
+ let metadata = input.metadata;
214
+ if (input.comms !== undefined) {
215
+ const base: Record<string, unknown> = {
216
+ ...(input.metadata !== undefined
217
+ ? (input.metadata ?? {})
218
+ : (before.metadata ?? {})),
219
+ };
220
+ if (input.comms === null) {
221
+ delete base.comms;
222
+ } else {
223
+ base.comms = input.comms;
224
+ }
225
+ metadata = Object.keys(base).length > 0 ? base : null;
226
+ }
227
+
195
228
  const user = updateUser(input.userId, {
196
229
  name: input.name,
197
230
  email: input.email,
@@ -202,7 +235,7 @@ export const registerManageUserTool = (server: McpServer) => {
202
235
  timezone: input.timezone,
203
236
  dailyBudgetUsd: input.dailyBudgetUsd,
204
237
  status: input.status,
205
- metadata: input.metadata,
238
+ metadata,
206
239
  });
207
240
  if (!user) {
208
241
  return toolErr(`User ${input.userId} not found.`);