@desplega.ai/agent-swarm 1.131.0 → 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 (265) hide show
  1. package/README.md +2 -2
  2. package/dist/{actions-w53mbns9.js → actions-cskpayd7.js} +11 -9
  3. package/dist/{anthropic-messages-rjmd2e7n.js → anthropic-messages-tpxtbx7b.js} +28 -12
  4. package/dist/{app-m9gwqsqv.js → app-ang77nbb.js} +8 -6
  5. package/dist/{assistant-bp65m6f3.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-62p0at2k.js → boot-reembed-ps2aftpb.js} +9 -7
  8. package/dist/{boot-reembed-xmfp6wdh.js → boot-reembed-znyk3nxb.js} +10 -8
  9. package/dist/{boot-scrub-logs-rr8q2dms.js → boot-scrub-logs-2ax3xgz1.js} +7 -5
  10. package/dist/{claude-adapter-2y47mrkd.js → claude-adapter-3ct8kgxw.js} +13 -9
  11. package/dist/{claude-managed-adapter-gv19tdew.js → claude-managed-adapter-akx4ntf2.js} +2 -2
  12. package/dist/{cli-t1xze5qh.js → cli-13w2hwkh.js} +1 -1
  13. package/dist/{cli-efxfnq6q.js → cli-1532szb1.js} +8 -6
  14. package/dist/{cli-d2tfkcga.js → cli-23d3rseb.js} +33 -17
  15. package/dist/{cli-nm5gtp1x.js → cli-2a75z9c5.js} +196 -7
  16. package/dist/{cli-fywms69x.js → cli-2t30rj0d.js} +1 -1
  17. package/dist/{cli-cvgjwnh1.js → cli-3c3tdgab.js} +10 -10
  18. package/dist/{cli-pm6xbh84.js → cli-3zzxa9jz.js} +46 -16
  19. package/dist/{cli-cdeg08wx.js → cli-4k6233gc.js} +1 -1
  20. package/dist/{cli-t55yw3a1.js → cli-5p2tz05r.js} +2 -2
  21. package/dist/{cli-0rcyq6d9.js → cli-5z2yhzvd.js} +125 -47
  22. package/dist/{cli-3kf51j15.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-72q8m8gk.js → cli-9nfc2es8.js} +6 -6
  26. package/dist/{cli-5ze9rv97.js → cli-9ta3pwms.js} +11 -4
  27. package/dist/{cli-78xwvkhj.js → cli-avr0ns7z.js} +2 -2
  28. package/dist/{cli-n6h8qt8n.js → cli-c897wyhw.js} +12 -7
  29. package/dist/{cli-2q72qz2s.js → cli-dgk20ft7.js} +15 -6
  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-e7ckcg4v.js → cli-ftwpv451.js} +170 -387
  34. package/dist/{cli-g6b8x1h8.js → cli-g854s2dw.js} +1 -1
  35. package/dist/{cli-5kx36evn.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-5v4rw6yd.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-rzbpmxdr.js → cli-r6tpm37h.js} +2 -2
  45. package/dist/{cli-qv61hp37.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-psh3c4va.js → cli-sfd9v7sj.js} +1 -1
  49. package/dist/{cli-70xqj0z3.js → cli-sh3ytsyj.js} +49 -39
  50. package/dist/{cli-zsspvvm2.js → cli-w1mp9jy7.js} +1 -1
  51. package/dist/{cli-d3x3x1c9.js → cli-xdb4mpnp.js} +2 -2
  52. package/dist/{cli-57khth04.js → cli-xmyqv00z.js} +120 -4
  53. package/dist/{cli-yw8vghx2.js → cli-xv8t8p3c.js} +65 -32
  54. package/dist/{cli-f1am9szv.js → cli-xxv5pn4f.js} +1 -1
  55. package/dist/{cli-t76vw5db.js → cli-y08jvxzx.js} +601 -140
  56. package/dist/{cli-a4jzdpa3.js → cli-zfbf3srj.js} +650 -245
  57. package/dist/{cli-a40rnf71.js → cli-zmq89k34.js} +6 -6
  58. package/dist/{cli-qmdnhpej.js → cli-zr9q011v.js} +23 -13
  59. package/dist/cli.js +22 -22
  60. package/dist/{codex-adapter-d24q75wk.js → codex-adapter-bef93pq4.js} +9 -7
  61. package/dist/{codex-hook-hq54t9ft.js → codex-hook-e8dba65p.js} +3 -2
  62. package/dist/{codex-session-runner-g50vcsex.js → codex-session-runner-5pkxys7x.js} +9 -7
  63. package/dist/{commands-c3nrc5hg.js → commands-e23s1bff.js} +9 -7
  64. package/dist/{db-nt7fg3tw.js → db-m3nabatn.js} +17 -5
  65. package/dist/{google-generative-ai-hysrd8qx.js → google-generative-ai-8maw55xh.js} +11 -8
  66. package/dist/{google-vertex-p5arrga0.js → google-vertex-8vjqmx8g.js} +11 -8
  67. package/dist/{handlers-fkr6swat.js → handlers-akazedvr.js} +16 -13
  68. package/dist/{hook-77wpvnjc.js → hook-ze4cpf0v.js} +55 -41
  69. package/dist/{hook-g019yehy.js → hook-zfqyyenw.js} +10 -7
  70. package/dist/{http-jewj0syk.js → http-sja0n6kw.js} +730 -150
  71. package/dist/index-0gr8vd6k.js +39 -0
  72. package/dist/{index-7n2xpbd4.js → index-354vkhdv.js} +154 -74
  73. package/dist/{index-070v4eyb.js → index-5qthek5t.js} +2 -3
  74. package/dist/{index-hfvggd0z.js → index-95t3cqq5.js} +16 -14
  75. package/dist/{index-9znhar3x.js → index-sea66619.js} +3 -4
  76. package/dist/{index-6gm5he8h.js → index-vpn36mma.js} +15 -13
  77. package/dist/{keepalive-j0fjvqfd.js → keepalive-3sp3meqj.js} +16 -10
  78. package/dist/{lead-1n8x30xe.js → lead-mefeewkf.js} +39 -36
  79. package/dist/{maintenance-gvev11sa.js → maintenance-8tbrbjfg.js} +10 -8
  80. package/dist/mistral-conversations-00zrp7g4.js +717 -0
  81. package/dist/{oauth-refresh-sweep-s9ecsvh0.js → oauth-refresh-sweep-srjwjz6m.js} +8 -6
  82. package/dist/{onboard-pvm5b38w.js → onboard-mt2tgyjg.js} +7 -7
  83. package/dist/{openai-codex-responses-5g27pd6f.js → openai-codex-responses-map32h2s.js} +18 -18
  84. package/dist/{openai-completions-3vkmfz2a.js → openai-completions-w871b1cq.js} +12 -11
  85. package/dist/{openai-responses-dg4t7j7a.js → openai-responses-b0j7xnw8.js} +15 -12
  86. package/dist/{opencode-adapter-1gm38aba.js → opencode-adapter-b39jefcj.js} +5 -3
  87. package/dist/{openrouter-images-g3gm8931.js → openrouter-images-srfkywy0.js} +4 -4
  88. package/dist/{otel-impl-p6jvgd9c.js → otel-impl-z6pxt3j9.js} +224 -9
  89. package/dist/{pi-mono-adapter-t091s27g.js → pi-mono-adapter-btvqmj58.js} +1138 -408
  90. package/dist/{pricing-refresh-k49rcyed.js → pricing-refresh-kyq8cezh.js} +9 -7
  91. package/dist/{rbac-roles-cr80g96t.js → rbac-roles-3ryfrfna.js} +8 -6
  92. package/dist/{rbac-roles-30pspvy7.js → rbac-roles-6qfvbee1.js} +9 -7
  93. package/dist/{render-v2-xm457ng1.js → render-v2-p61ppjey.js} +8 -6
  94. package/dist/{seed-pricing-yhpv35fr.js → seed-pricing-j56bbj4y.js} +8 -6
  95. package/dist/{setup-nytnmwwr.js → setup-tpr6c1kn.js} +2 -2
  96. package/dist/{worker-msytr2cw.js → worker-9w1v0mbb.js} +39 -36
  97. package/openapi.json +542 -16
  98. package/package.json +6 -6
  99. package/src/be/budget-refusal-notify.ts +14 -7
  100. package/src/be/db-queries/task-artifacts.ts +182 -0
  101. package/src/be/db.ts +783 -169
  102. package/src/be/events.ts +5 -0
  103. package/src/be/migrations/130_scratch_script_retention_grace.sql +6 -0
  104. package/src/be/migrations/131_session_logs_created_at_index.sql +2 -0
  105. package/src/be/migrations/132_multi_runtime_instances.sql +44 -0
  106. package/src/be/migrations/133_workflow_run_trigger_type.sql +12 -0
  107. package/src/be/migrations/135_backfill_task_pull_request_attachments.sql +228 -0
  108. package/src/be/migrations/136_task_requester_provenance.sql +37 -0
  109. package/src/be/migrations/137_memory_retrieval_composite_index.sql +1 -0
  110. package/src/be/multi-runtime.ts +430 -0
  111. package/src/be/scripts/db.ts +43 -1
  112. package/src/be/scripts/extract-schema.ts +3 -1
  113. package/src/be/scripts/retention.ts +164 -0
  114. package/src/be/scripts/run-saved.ts +16 -1
  115. package/src/be/scripts/typecheck.ts +3 -2
  116. package/src/be/seed-skills/index.ts +7 -0
  117. package/src/be/steering.ts +1 -1
  118. package/src/be/swarm-config-guard.ts +18 -0
  119. package/src/commands/credential-wait.ts +54 -0
  120. package/src/commands/onboard/steps/core-credentials.tsx +1 -1
  121. package/src/commands/onboard/steps/custom-templates.tsx +1 -1
  122. package/src/commands/onboard/steps/harness-credentials.tsx +1 -1
  123. package/src/commands/onboard/steps/health-check.tsx +1 -1
  124. package/src/commands/onboard/steps/start.tsx +1 -1
  125. package/src/commands/profile-sync-audit.ts +56 -0
  126. package/src/commands/profile-sync.ts +287 -15
  127. package/src/commands/provider-credentials.ts +46 -19
  128. package/src/commands/runner.ts +196 -33
  129. package/src/commands/templates.ts +22 -0
  130. package/src/github/handlers.ts +55 -9
  131. package/src/heartbeat/heartbeat.ts +71 -12
  132. package/src/hooks/hook.ts +49 -33
  133. package/src/http/active-sessions.ts +11 -1
  134. package/src/http/agents.ts +261 -30
  135. package/src/http/all-routes.ts +1 -0
  136. package/src/http/config.ts +13 -2
  137. package/src/http/core.ts +102 -4
  138. package/src/http/db-query-bounded.ts +313 -0
  139. package/src/http/db-query-shared.ts +249 -0
  140. package/src/http/db-query.ts +54 -42
  141. package/src/http/events.ts +2 -0
  142. package/src/http/index.ts +27 -2
  143. package/src/http/mcp-bridge.ts +37 -3
  144. package/src/http/mcp.ts +12 -0
  145. package/src/http/memory.ts +13 -2
  146. package/src/http/openapi.ts +1 -0
  147. package/src/http/poll.ts +70 -27
  148. package/src/http/route-def.ts +43 -9
  149. package/src/http/schedules.ts +2 -2
  150. package/src/http/scripts.ts +31 -4
  151. package/src/http/session-data.ts +44 -0
  152. package/src/http/tasks.ts +2 -2
  153. package/src/http/users.ts +74 -52
  154. package/src/http/webhooks.ts +5 -2
  155. package/src/http/workflows.ts +1 -4
  156. package/src/http/x.ts +14 -0
  157. package/src/integrations/kapso/inbound.ts +1 -1
  158. package/src/jira/outbound.ts +39 -8
  159. package/src/jira/webhook-lifecycle.ts +13 -2
  160. package/src/linear/outbound.ts +43 -10
  161. package/src/oauth/keepalive.ts +13 -2
  162. package/src/prompts/base-prompt.ts +1 -1
  163. package/src/prompts/session-templates.ts +45 -7
  164. package/src/providers/claude-adapter.ts +6 -0
  165. package/src/providers/claude-managed-models.ts +34 -10
  166. package/src/providers/codex-adapter.ts +6 -0
  167. package/src/providers/opencode-adapter.ts +6 -0
  168. package/src/providers/pi-mono-adapter.ts +8 -0
  169. package/src/providers/pricing-sources.md +24 -0
  170. package/src/queue-stall-alarm.ts +216 -0
  171. package/src/scheduler/scheduler.ts +7 -0
  172. package/src/script-workflows/executor.ts +2 -1
  173. package/src/scripts-runtime/executors/native.ts +2 -1
  174. package/src/scripts-runtime/executors/types.ts +5 -0
  175. package/src/scripts-runtime/loader.ts +5 -0
  176. package/src/scripts-runtime/swarm-config.ts +8 -0
  177. package/src/scripts-runtime/swarm-sdk.ts +7 -0
  178. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  179. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  180. package/src/tests/budget-claim-gate.test.ts +20 -0
  181. package/src/tests/claude-adapter.test.ts +32 -0
  182. package/src/tests/claude-managed-adapter.test.ts +38 -7
  183. package/src/tests/codex-adapter.test.ts +30 -0
  184. package/src/tests/create-task-validation.test.ts +128 -0
  185. package/src/tests/credential-wait.test.ts +67 -1
  186. package/src/tests/db-query.test.ts +711 -2
  187. package/src/tests/http-api-integration.test.ts +81 -0
  188. package/src/tests/http-users.test.ts +166 -1
  189. package/src/tests/identity-field-budget.test.ts +176 -0
  190. package/src/tests/mcp-tools-user.test.ts +34 -0
  191. package/src/tests/mcp-user-route.test.ts +17 -0
  192. package/src/tests/metrics-http.test.ts +44 -0
  193. package/src/tests/multi-runtime-registration.test.ts +2703 -0
  194. package/src/tests/openapi-response-contract.test.ts +24 -0
  195. package/src/tests/openapi-route-headers.test.ts +83 -0
  196. package/src/tests/opencode-adapter.test.ts +40 -0
  197. package/src/tests/outbound-listener-rejection.test.ts +138 -0
  198. package/src/tests/pi-mono-adapter.test.ts +98 -0
  199. package/src/tests/profile-sync-warning.test.ts +277 -0
  200. package/src/tests/profile-sync.test.ts +232 -6
  201. package/src/tests/prompt-template-session.test.ts +32 -3
  202. package/src/tests/queue-stall-alarm.test.ts +226 -0
  203. package/src/tests/requester-comms.test.ts +32 -0
  204. package/src/tests/runner-requester-profile.test.ts +38 -0
  205. package/src/tests/sandboxed-process.test.ts +45 -0
  206. package/src/tests/schedule-target-type.test.ts +38 -0
  207. package/src/tests/script-connections.test.ts +16 -3
  208. package/src/tests/scripts-external-api.test.ts +8 -3
  209. package/src/tests/scripts-http.test.ts +12 -0
  210. package/src/tests/scripts-retention.test.ts +384 -0
  211. package/src/tests/scripts-runtime-identity.test.ts +375 -0
  212. package/src/tests/scripts-runtime.test.ts +4 -3
  213. package/src/tests/sdk-allowlist.test.ts +47 -0
  214. package/src/tests/send-task-requested-by.test.ts +9 -0
  215. package/src/tests/session-costs.test.ts +506 -1
  216. package/src/tests/shutdown-error-scrubbing.test.ts +92 -0
  217. package/src/tests/steering-core.test.ts +28 -0
  218. package/src/tests/task-artifacts.test.ts +386 -0
  219. package/src/tests/task-attachments-schema.test.ts +17 -1
  220. package/src/tests/task-completion-idempotency.test.ts +57 -0
  221. package/src/tests/task-pull-request-backfill-migration.test.ts +321 -0
  222. package/src/tests/update-profile-api.test.ts +67 -39
  223. package/src/tests/update-profile-auth.test.ts +48 -4
  224. package/src/tests/user-attribution-migration.test.ts +54 -0
  225. package/src/tests/user-identity.test.ts +72 -0
  226. package/src/tests/workflow-async-v2.test.ts +39 -1
  227. package/src/tests/workflow-http-v2.test.ts +3 -2
  228. package/src/tests/workflow-mcp-trigger-schema.test.ts +52 -3
  229. package/src/tests/workflow-retry-v2.test.ts +45 -1
  230. package/src/tests/workflow-swarm-script.test.ts +14 -5
  231. package/src/tools/db-query.ts +18 -8
  232. package/src/tools/get-task-details.ts +24 -3
  233. package/src/tools/manage-user.ts +36 -3
  234. package/src/tools/poll-task.ts +93 -14
  235. package/src/tools/script-common.ts +5 -0
  236. package/src/tools/send-task.ts +1 -4
  237. package/src/tools/store-progress.ts +13 -2
  238. package/src/tools/swarm-config/delete-config.ts +9 -0
  239. package/src/tools/swarm-config/set-config.ts +3 -2
  240. package/src/tools/task-action.ts +30 -0
  241. package/src/tools/task-tool-ctx.ts +8 -1
  242. package/src/tools/update-profile.ts +16 -21
  243. package/src/tools/utils.ts +5 -0
  244. package/src/tools/workflows/trigger-workflow.ts +7 -1
  245. package/src/types.ts +165 -1
  246. package/src/utils/github-pull-request.ts +76 -0
  247. package/src/utils/identity-field-budget.ts +73 -0
  248. package/src/utils/multi-runtime.ts +20 -0
  249. package/src/utils/requester-comms.ts +24 -0
  250. package/src/utils/sandboxed-process.ts +40 -14
  251. package/src/workflows/engine.ts +22 -2
  252. package/src/workflows/executors/swarm-script.ts +24 -1
  253. package/src/workflows/resume.ts +19 -11
  254. package/src/workflows/retry-poller.ts +1 -0
  255. package/src/workflows/triggers.ts +4 -0
  256. package/templates/schedules/daily-compounding-reflection/content.md +12 -3
  257. package/templates/skills/db-query-guidance/config.json +14 -0
  258. package/templates/skills/db-query-guidance/content.md +57 -0
  259. package/dist/cli-3te4jdkm.js +0 -225
  260. package/dist/cli-bmze986d.js +0 -7
  261. package/dist/cli-kh3ba2zf.js +0 -607
  262. package/dist/index-hp1kdbjj.js +0 -36
  263. package/dist/mistral-conversations-81fwav0j.js +0 -30578
  264. package/dist/otel-nqa12wf3.js +0 -3149
  265. package/dist/{pi-messages-tqj8q6b9.js → pi-messages-sssy32x4.js} +3 -3
@@ -11,6 +11,7 @@ import type { Subprocess } from "bun";
11
11
  import { Webhook } from "svix";
12
12
  import { slackContextKey } from "../tasks/context-key";
13
13
  import { MAX_PROFILE_FILE_LENGTH } from "../utils/constants";
14
+ import { SOUL_MD_MAX_CHARS } from "../utils/identity-field-budget";
14
15
 
15
16
  const TEST_PORT = Number(
16
17
  process.env.HTTP_API_INTEGRATION_TEST_PORT ?? 30000 + Math.floor(Math.random() * 20000),
@@ -315,6 +316,86 @@ describe("Agents", () => {
315
316
  expect(rejected.status).toBe(400);
316
317
  });
317
318
 
319
+ test("PUT /api/agents/:id/profile — persists structured filesystem sync rejections", async () => {
320
+ const current = "s".repeat(SOUL_MD_MAX_CHARS);
321
+ const accepted = await put(`/api/agents/${ids.workerAgent}/profile`, {
322
+ body: { soulMd: current, changeSource: "session_sync" },
323
+ agentId: ids.workerAgent,
324
+ });
325
+ expect(accepted.status).toBe(200);
326
+
327
+ const rejected = await put(`/api/agents/${ids.workerAgent}/profile`, {
328
+ body: { soulMd: `${current}x`, changeSource: "session_sync" },
329
+ agentId: ids.workerAgent,
330
+ });
331
+ expect(rejected.status).toBe(400);
332
+ expect(rejected.body.profileSyncRejection).toMatchObject({
333
+ field: "soulMd",
334
+ diskSize: SOUL_MD_MAX_CHARS + 1,
335
+ dbSize: SOUL_MD_MAX_CHARS,
336
+ budget: SOUL_MD_MAX_CHARS,
337
+ delta: 1,
338
+ });
339
+
340
+ const query = new URLSearchParams({
341
+ event: "system.profile_sync_rejected",
342
+ agentId: ids.workerAgent,
343
+ dataField: "soulMd",
344
+ limit: "1",
345
+ });
346
+ const events = await get(`/api/events?${query}`, { agentId: ids.workerAgent });
347
+ expect(events.status).toBe(200);
348
+ expect(events.body.events).toHaveLength(1);
349
+ expect(events.body.events[0]).toMatchObject({
350
+ category: "system",
351
+ event: "system.profile_sync_rejected",
352
+ status: "error",
353
+ source: "api",
354
+ agentId: ids.workerAgent,
355
+ data: {
356
+ field: "soulMd",
357
+ diskSize: SOUL_MD_MAX_CHARS + 1,
358
+ dbSize: SOUL_MD_MAX_CHARS,
359
+ budget: SOUL_MD_MAX_CHARS,
360
+ delta: 1,
361
+ changeSource: "session_sync",
362
+ },
363
+ });
364
+
365
+ const reconciled = await put(`/api/agents/${ids.workerAgent}/profile`, {
366
+ body: { soulMd: current, changeSource: "session_sync" },
367
+ agentId: ids.workerAgent,
368
+ });
369
+ expect(reconciled.status).toBe(200);
370
+ const reconciliationEvents = await get(
371
+ `/api/events?${new URLSearchParams({
372
+ event: "system.profile_sync_reconciled",
373
+ agentId: ids.workerAgent,
374
+ dataField: "soulMd",
375
+ limit: "1",
376
+ })}`,
377
+ { agentId: ids.workerAgent },
378
+ );
379
+ expect(reconciliationEvents.status).toBe(200);
380
+ expect(reconciliationEvents.body.events[0]).toMatchObject({
381
+ event: "system.profile_sync_reconciled",
382
+ status: "ok",
383
+ data: { field: "soulMd", changeSource: "session_sync" },
384
+ });
385
+
386
+ const unrelated = await get(
387
+ `/api/events?${new URLSearchParams({
388
+ event: "system.profile_sync_rejected",
389
+ agentId: ids.workerAgent,
390
+ dataField: "toolsMd",
391
+ limit: "1",
392
+ })}`,
393
+ { agentId: ids.workerAgent },
394
+ );
395
+ expect(unrelated.status).toBe(200);
396
+ expect(unrelated.body.events).toHaveLength(0);
397
+ });
398
+
318
399
  test("PUT /api/agents/:id/profile — non-existent returns 404", async () => {
319
400
  const { status } = await put(`/api/agents/${randomUUID()}/profile`, {
320
401
  body: { role: "ghost" },
@@ -20,7 +20,20 @@ import {
20
20
  type Server,
21
21
  type ServerResponse,
22
22
  } from "node:http";
23
- import { closeDb, createUser, getBudget, getDb, initDb, upsertKv } from "../be/db";
23
+ import {
24
+ closeDb,
25
+ createScheduledTask,
26
+ createUser,
27
+ createWorkflow,
28
+ createWorkflowRun,
29
+ getBudget,
30
+ getDb,
31
+ getScheduledTaskById,
32
+ getWorkflowRun,
33
+ initDb,
34
+ updateWorkflowRun,
35
+ upsertKv,
36
+ } from "../be/db";
24
37
  import { fingerprintApiKey, linkIdentity } from "../be/users";
25
38
  import { handleCore } from "../http/core";
26
39
  import { handleUsers } from "../http/users";
@@ -91,6 +104,10 @@ beforeEach(() => {
91
104
  db.run("DELETE FROM user_identity_events");
92
105
  db.run("DELETE FROM user_external_ids");
93
106
  db.run("DELETE FROM user_tokens");
107
+ db.run("DELETE FROM workflow_run_steps");
108
+ db.run("DELETE FROM workflow_runs");
109
+ db.run("DELETE FROM workflows");
110
+ db.run("DELETE FROM scheduled_tasks");
94
111
  db.run("DELETE FROM users");
95
112
  db.run("DELETE FROM budgets");
96
113
  db.run("DELETE FROM kv_entries");
@@ -325,6 +342,56 @@ describe("PATCH /api/users/:id", () => {
325
342
  expect(fields).toContain("name");
326
343
  expect(fields).not.toContain("role");
327
344
  });
345
+
346
+ test("comms merges into metadata.comms and preserves sibling metadata keys", async () => {
347
+ const u = createUser({ name: "CommsMerge", metadata: { customerId: "abc123" } });
348
+
349
+ const r = await authedFetch(`/api/users/${u.id}`, {
350
+ method: "PATCH",
351
+ body: JSON.stringify({
352
+ comms: { tone: "casual", verbosity: "short" },
353
+ }),
354
+ });
355
+ expect(r.status).toBe(200);
356
+ const { user } = (await r.json()) as { user: { metadata: Record<string, unknown> | null } };
357
+ expect(user.metadata).toEqual({
358
+ customerId: "abc123",
359
+ comms: { tone: "casual", verbosity: "short" },
360
+ });
361
+ });
362
+
363
+ test("comms: null removes only the comms key", async () => {
364
+ const u = createUser({
365
+ name: "CommsRemove",
366
+ metadata: { customerId: "abc123", comms: { tone: "formal" } },
367
+ });
368
+
369
+ const r = await authedFetch(`/api/users/${u.id}`, {
370
+ method: "PATCH",
371
+ body: JSON.stringify({ comms: null }),
372
+ });
373
+ expect(r.status).toBe(200);
374
+ const { user } = (await r.json()) as { user: { metadata: Record<string, unknown> | null } };
375
+ expect(user.metadata).toEqual({ customerId: "abc123" });
376
+ });
377
+
378
+ test("comms combined with metadata applies metadata first then comms", async () => {
379
+ const u = createUser({ name: "CommsWithMetadata", metadata: { customerId: "abc123" } });
380
+
381
+ const r = await authedFetch(`/api/users/${u.id}`, {
382
+ method: "PATCH",
383
+ body: JSON.stringify({
384
+ metadata: { region: "eu" },
385
+ comms: { tone: "formal" },
386
+ }),
387
+ });
388
+ expect(r.status).toBe(200);
389
+ const { user } = (await r.json()) as { user: { metadata: Record<string, unknown> | null } };
390
+ expect(user.metadata).toEqual({
391
+ region: "eu",
392
+ comms: { tone: "formal" },
393
+ });
394
+ });
328
395
  });
329
396
 
330
397
  describe("identity link/unlink", () => {
@@ -620,6 +687,24 @@ describe("POST /api/users/:id/merge", () => {
620
687
  const actor = { kind: "operator" as const, id: OPERATOR_FP };
621
688
  linkIdentity(source.id, "slack", "U_SRC", actor);
622
689
  linkIdentity(source.id, "github", "src-gh", actor);
690
+ const workflow = createWorkflow({
691
+ name: `merge-user-workflow-${crypto.randomUUID()}`,
692
+ definition: { nodes: [] },
693
+ });
694
+ const run = createWorkflowRun({
695
+ id: crypto.randomUUID(),
696
+ workflowId: workflow.id,
697
+ createdBy: source.id,
698
+ });
699
+ updateWorkflowRun(run.id, {
700
+ context: { swarm: { requestedByUserId: source.id }, retained: "value" },
701
+ });
702
+ const schedule = createScheduledTask({
703
+ name: `merge-user-schedule-${crypto.randomUUID()}`,
704
+ intervalMs: 60_000,
705
+ taskTemplate: "Run merged schedule",
706
+ createdBy: source.id,
707
+ });
623
708
 
624
709
  const r = await authedFetch(`/api/users/${target.id}/merge`, {
625
710
  method: "POST",
@@ -647,6 +732,13 @@ describe("POST /api/users/:id/merge", () => {
647
732
  // Source user is gone.
648
733
  const sourceR = await authedFetch(`/api/users/${source.id}`);
649
734
  expect(sourceR.status).toBe(404);
735
+ expect(getWorkflowRun(run.id)?.createdBy).toBe(target.id);
736
+ expect(getWorkflowRun(run.id)?.context).toEqual({
737
+ swarm: { requestedByUserId: target.id },
738
+ retained: "value",
739
+ });
740
+ expect(getScheduledTaskById(schedule.id)?.createdBy).toBe(target.id);
741
+ expect(getScheduledTaskById(schedule.id)?.updatedBy).toBe(target.id);
650
742
  });
651
743
 
652
744
  test("manual_merge event payload carries the source user's id/name", async () => {
@@ -675,6 +767,79 @@ describe("POST /api/users/:id/merge", () => {
675
767
  expect(mergeEvent!.after?.source?.email).toBe("ms@x.com");
676
768
  });
677
769
 
770
+ test("rolls back identity moves when deletion fails", async () => {
771
+ const target = createUser({ name: "RollbackTarget", email: "rollback-target@x.com" });
772
+ const source = createUser({ name: "RollbackSource", email: "rollback-source@x.com" });
773
+ const workflow = createWorkflow({
774
+ name: `rollback-attribution-${crypto.randomUUID()}`,
775
+ definition: { nodes: [] },
776
+ });
777
+ const run = createWorkflowRun({
778
+ id: crypto.randomUUID(),
779
+ workflowId: workflow.id,
780
+ createdBy: source.id,
781
+ });
782
+ updateWorkflowRun(run.id, {
783
+ context: { swarm: { requestedByUserId: source.id }, retained: "rollback" },
784
+ });
785
+ const schedule = createScheduledTask({
786
+ name: `rollback-user-schedule-${crypto.randomUUID()}`,
787
+ intervalMs: 60_000,
788
+ taskTemplate: "Retain rollback schedule",
789
+ createdBy: source.id,
790
+ });
791
+ linkIdentity(source.id, "slack", "U_MERGE_ROLLBACK", {
792
+ kind: "operator",
793
+ id: OPERATOR_FP,
794
+ });
795
+ getDb().run(`CREATE TEMP TRIGGER fail_merge_source_delete
796
+ BEFORE DELETE ON users WHEN OLD.id = '${source.id}'
797
+ BEGIN SELECT RAISE(ABORT, 'forced merge failure'); END`);
798
+
799
+ try {
800
+ const response = await authedFetch(`/api/users/${target.id}/merge`, {
801
+ method: "POST",
802
+ body: JSON.stringify({ sourceUserId: source.id }),
803
+ });
804
+ expect(response.status).toBe(500);
805
+
806
+ const identity = getDb()
807
+ .prepare<{ userId: string }, [string, string]>(
808
+ "SELECT userId FROM user_external_ids WHERE kind = ? AND externalId = ?",
809
+ )
810
+ .get("slack", "U_MERGE_ROLLBACK");
811
+ expect(identity?.userId).toBe(source.id);
812
+ expect(
813
+ getDb()
814
+ .prepare<{ count: number }, [string]>("SELECT COUNT(*) AS count FROM users WHERE id = ?")
815
+ .get(source.id)?.count,
816
+ ).toBe(1);
817
+ expect(
818
+ getDb()
819
+ .prepare<{ emailAliases: string }, [string]>(
820
+ "SELECT emailAliases FROM users WHERE id = ?",
821
+ )
822
+ .get(target.id)?.emailAliases,
823
+ ).toBe("[]");
824
+ expect(
825
+ getDb()
826
+ .prepare<{ count: number }, [string]>(
827
+ "SELECT COUNT(*) AS count FROM user_identity_events WHERE userId = ?",
828
+ )
829
+ .get(target.id)?.count,
830
+ ).toBe(0);
831
+ expect(getWorkflowRun(run.id)?.createdBy).toBe(source.id);
832
+ expect(getWorkflowRun(run.id)?.context).toEqual({
833
+ swarm: { requestedByUserId: source.id },
834
+ retained: "rollback",
835
+ });
836
+ expect(getScheduledTaskById(schedule.id)?.createdBy).toBe(source.id);
837
+ expect(getScheduledTaskById(schedule.id)?.updatedBy).toBe(source.id);
838
+ } finally {
839
+ getDb().run("DROP TRIGGER fail_merge_source_delete");
840
+ }
841
+ });
842
+
678
843
  test("400 when target == source", async () => {
679
844
  const u = createUser({ name: "Self" });
680
845
  const r = await authedFetch(`/api/users/${u.id}/merge`, {
@@ -0,0 +1,176 @@
1
+ import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
2
+ import { closeDb, createAgent, getAgentById, getDb, initDb, updateAgentProfile } from "../be/db";
3
+ import { postHookProfileUpdate } from "../hooks/hook";
4
+ import { BOOTSTRAP_MAX_CHARS } from "../prompts/base-prompt";
5
+ import {
6
+ checkIdentityFieldBudget,
7
+ IDENTITY_FIELD_BUDGETS,
8
+ IDENTITY_MD_MAX_CHARS,
9
+ IdentityFieldBudgetError,
10
+ SOUL_MD_MAX_CHARS,
11
+ } from "../utils/identity-field-budget";
12
+
13
+ const TEST_DB_PATH = "./test-identity-field-budget.sqlite";
14
+
15
+ describe("checkIdentityFieldBudget", () => {
16
+ test("exports the session-injection budgets from one policy module", () => {
17
+ expect(SOUL_MD_MAX_CHARS).toBe(10_000);
18
+ expect(IDENTITY_MD_MAX_CHARS).toBe(10_000);
19
+ expect(IDENTITY_FIELD_BUDGETS.claudeMd).toBe(BOOTSTRAP_MAX_CHARS);
20
+ expect(IDENTITY_FIELD_BUDGETS.toolsMd).toBe(BOOTSTRAP_MAX_CHARS);
21
+ });
22
+
23
+ test("accepts writes at or below budget, including growth", () => {
24
+ expect(
25
+ checkIdentityFieldBudget({
26
+ field: "soulMd",
27
+ currentValue: "x",
28
+ nextValue: "x".repeat(SOUL_MD_MAX_CHARS),
29
+ }),
30
+ ).toEqual({ ok: true });
31
+ });
32
+
33
+ test("accepts a shrinking write while the result remains over budget", () => {
34
+ expect(
35
+ checkIdentityFieldBudget({
36
+ field: "identityMd",
37
+ currentValue: "x".repeat(12_000),
38
+ nextValue: "x".repeat(11_999),
39
+ }),
40
+ ).toEqual({ ok: true });
41
+ });
42
+
43
+ test("rejects growth and accepts equal-length rewrites while over budget", () => {
44
+ const growing = checkIdentityFieldBudget({
45
+ field: "claudeMd",
46
+ currentValue: "x".repeat(BOOTSTRAP_MAX_CHARS + 1),
47
+ nextValue: "x".repeat(BOOTSTRAP_MAX_CHARS + 2),
48
+ });
49
+ const equal = checkIdentityFieldBudget({
50
+ field: "toolsMd",
51
+ currentValue: "x".repeat(BOOTSTRAP_MAX_CHARS + 1),
52
+ nextValue: "y".repeat(BOOTSTRAP_MAX_CHARS + 1),
53
+ });
54
+
55
+ expect(growing.ok).toBeFalse();
56
+ expect(equal).toEqual({ ok: true });
57
+ if (growing.ok) throw new Error("Expected budget rejection");
58
+ expect(growing.reason).toContain("claudeMd");
59
+ expect(growing.reason).toContain("current size 20001");
60
+ expect(growing.reason).toContain("budget 20000");
61
+ expect(growing.reason).toContain("delta +1");
62
+ expect(growing.reason).toBe(
63
+ "Update rejected for claudeMd: current size 20001 characters, budget 20000 characters, delta +1 characters." +
64
+ " The tail past the 20000-character cap is dropped from the base prompt and only reaches harnesses with a native CLAUDE.md loader." +
65
+ " Move durable content into memories and keep pointers to it in this field.",
66
+ );
67
+
68
+ const toolsGrowth = checkIdentityFieldBudget({
69
+ field: "toolsMd",
70
+ currentValue: "x".repeat(BOOTSTRAP_MAX_CHARS + 1),
71
+ nextValue: "x".repeat(BOOTSTRAP_MAX_CHARS + 2),
72
+ });
73
+ if (toolsGrowth.ok) throw new Error("Expected toolsMd budget rejection");
74
+ expect(toolsGrowth.reason).toBe(
75
+ "Update rejected for toolsMd: current size 20001 characters, budget 20000 characters, delta +1 characters." +
76
+ " Content past the 20000-character cap is already silently dropped at read time, so shrinking that tail loses nothing sessions currently receive." +
77
+ " Move durable content into memories and keep pointers to it in this field.",
78
+ );
79
+ });
80
+ });
81
+
82
+ describe("updateAgentProfile identity budget enforcement", () => {
83
+ const agentId = "identity-budget-agent";
84
+
85
+ beforeAll(async () => {
86
+ for (const suffix of ["", "-wal", "-shm"]) {
87
+ await Bun.file(TEST_DB_PATH + suffix)
88
+ .delete()
89
+ .catch(() => {});
90
+ }
91
+ initDb(TEST_DB_PATH);
92
+ createAgent({ id: agentId, name: "Identity Budget Agent", isLead: false, status: "idle" });
93
+ });
94
+
95
+ afterAll(async () => {
96
+ closeDb();
97
+ for (const suffix of ["", "-wal", "-shm"]) {
98
+ await Bun.file(TEST_DB_PATH + suffix)
99
+ .delete()
100
+ .catch(() => {});
101
+ }
102
+ });
103
+
104
+ test("preserves in-budget behavior and leaves heartbeatMd ungated", () => {
105
+ const heartbeat = "h".repeat(25_000);
106
+ const result = updateAgentProfile(agentId, {
107
+ soulMd: "s".repeat(SOUL_MD_MAX_CHARS),
108
+ heartbeatMd: heartbeat,
109
+ });
110
+
111
+ expect(result?.soulMd).toHaveLength(SOUL_MD_MAX_CHARS);
112
+ expect(result?.heartbeatMd).toBe(heartbeat);
113
+ });
114
+
115
+ test("allows an existing oversized value to shrink or change at equal length but rejects growth", () => {
116
+ const current = "c".repeat(BOOTSTRAP_MAX_CHARS + 10);
117
+ getDb().prepare("UPDATE agents SET claudeMd = ? WHERE id = ?").run(current, agentId);
118
+
119
+ const shrunk = "s".repeat(BOOTSTRAP_MAX_CHARS + 9);
120
+ expect(updateAgentProfile(agentId, { claudeMd: shrunk })?.claudeMd).toBe(shrunk);
121
+
122
+ const equalLength = "e".repeat(shrunk.length);
123
+ expect(updateAgentProfile(agentId, { claudeMd: equalLength })?.claudeMd).toBe(equalLength);
124
+ expect(() => updateAgentProfile(agentId, { claudeMd: `${equalLength}g` })).toThrow(
125
+ IdentityFieldBudgetError,
126
+ );
127
+ expect(getAgentById(agentId)?.claudeMd).toBe(equalLength);
128
+ });
129
+ });
130
+
131
+ describe("Stop-hook profile sync visibility", () => {
132
+ test("logs a rejected response with the server's budget reason", async () => {
133
+ const reason =
134
+ "Update rejected for toolsMd: current size 20001 characters, budget 20000 characters, delta +1 characters.";
135
+ const errorSpy = spyOn(console, "error").mockImplementation(() => {});
136
+ try {
137
+ await postHookProfileUpdate({
138
+ url: "https://api.example.test/api/agents/agent-1/profile",
139
+ headers: { Authorization: "Bearer secret", "X-Agent-ID": "agent-1" },
140
+ body: { toolsMd: "next", changeSource: "session_sync" },
141
+ label: "identity",
142
+ fetchImpl: (async () =>
143
+ new Response(JSON.stringify({ error: reason }), { status: 400 })) as typeof fetch,
144
+ });
145
+
146
+ expect(errorSpy).toHaveBeenCalledTimes(1);
147
+ const logged = String(errorSpy.mock.calls[0]?.[0]);
148
+ expect(logged).toContain("identity profile sync failed: HTTP 400");
149
+ expect(logged).toContain(reason);
150
+ } finally {
151
+ errorSpy.mockRestore();
152
+ }
153
+ });
154
+
155
+ test("logs thrown sync errors without failing shutdown", async () => {
156
+ const errorSpy = spyOn(console, "error").mockImplementation(() => {});
157
+ try {
158
+ await expect(
159
+ postHookProfileUpdate({
160
+ url: "https://api.example.test/api/agents/agent-1/profile",
161
+ headers: {},
162
+ body: { claudeMd: "next" },
163
+ label: "claudeMd",
164
+ fetchImpl: (async () => {
165
+ throw new Error("network unavailable");
166
+ }) as typeof fetch,
167
+ }),
168
+ ).resolves.toBeUndefined();
169
+ expect(String(errorSpy.mock.calls[0]?.[0])).toContain(
170
+ "claudeMd profile sync errored: network unavailable",
171
+ );
172
+ } finally {
173
+ errorSpy.mockRestore();
174
+ }
175
+ });
176
+ });
@@ -436,4 +436,38 @@ describe("manage-user MCP tool (identities array)", () => {
436
436
  const idents = getUserIdentities(userId);
437
437
  expect(idents).toHaveLength(0);
438
438
  });
439
+
440
+ test("update with comms merges into metadata.comms and preserves sibling metadata keys", async () => {
441
+ const created = createUser({ name: "Comms Merge", metadata: { customerId: "abc123" } });
442
+
443
+ const result = await callTool(server, "manage-user", {
444
+ action: "update",
445
+ userId: created.id,
446
+ comms: { tone: "casual", verbosity: "short" },
447
+ });
448
+
449
+ expect(result.isError).toBe(false);
450
+ const user = structuredOf(result).user as { metadata: Record<string, unknown> | null };
451
+ expect(user.metadata).toEqual({
452
+ customerId: "abc123",
453
+ comms: { tone: "casual", verbosity: "short" },
454
+ });
455
+ });
456
+
457
+ test("update with comms: null removes only the comms key", async () => {
458
+ const created = createUser({
459
+ name: "Comms Remove",
460
+ metadata: { customerId: "abc123", comms: { tone: "formal" } },
461
+ });
462
+
463
+ const result = await callTool(server, "manage-user", {
464
+ action: "update",
465
+ userId: created.id,
466
+ comms: null,
467
+ });
468
+
469
+ expect(result.isError).toBe(false);
470
+ const user = structuredOf(result).user as { metadata: Record<string, unknown> | null };
471
+ expect(user.metadata).toEqual({ customerId: "abc123" });
472
+ });
439
473
  });
@@ -362,6 +362,23 @@ describe("/mcp-user auth and tool surface", () => {
362
362
  expect(unknown.response.status).toBe(401);
363
363
  });
364
364
 
365
+ test("owner /mcp returns method not found for sessionless server/discover", async () => {
366
+ const { response, payload } = await mcpPost(
367
+ API_KEY,
368
+ { jsonrpc: "2.0", id: 7, method: "server/discover", params: {} },
369
+ undefined,
370
+ "/mcp",
371
+ );
372
+
373
+ expect(response.status).toBe(200);
374
+ expect(response.headers.get("mcp-session-id")).toBeNull();
375
+ expect(payload).toEqual({
376
+ jsonrpc: "2.0",
377
+ error: { code: -32601, message: "Method not found" },
378
+ id: 7,
379
+ });
380
+ });
381
+
365
382
  test("owner /mcp path initializes with a known agent and rejects a different X-Agent-ID on the session", async () => {
366
383
  const owner = createAgent({ name: "Owner MCP Agent", isLead: false, status: "idle" });
367
384
  const other = createAgent({ name: "Other MCP Agent", isLead: false, status: "idle" });
@@ -22,6 +22,7 @@ type MetricRunResponse = {
22
22
  result: {
23
23
  columns: string[];
24
24
  rows: Record<string, unknown>[];
25
+ truncated?: boolean;
25
26
  };
26
27
  }>;
27
28
  result: {
@@ -378,4 +379,47 @@ describe("Metrics HTTP API", () => {
378
379
  expect(res.status).toBe(400);
379
380
  }
380
381
  });
382
+
383
+ // Test D (Fix 1, db-query bounded-execution proposal §8): `truncated` still
384
+ // reaches the metrics response. Metric queries stay on the synchronous
385
+ // executeReadOnlyQuery path in this change (proposal §5.1's closing note),
386
+ // so this passes today via runMetricWidget's `total > rows.length` check —
387
+ // it must keep passing once db-query's other two call sites move to the
388
+ // bounded path.
389
+ test("D: a widget whose query returns more rows than maxRows reports truncated: true", async () => {
390
+ const created = await fetch(`${BASE}/api/metrics/definitions`, {
391
+ method: "POST",
392
+ headers,
393
+ body: JSON.stringify({
394
+ slug: "truncated-widget",
395
+ title: "Truncated Widget",
396
+ definition: {
397
+ version: 1,
398
+ widgets: [
399
+ {
400
+ id: "many-rows",
401
+ title: "Many rows",
402
+ query: {
403
+ sql: `WITH RECURSIVE cnt(x) AS (SELECT 1 UNION ALL SELECT x + 1 FROM cnt WHERE x < 50) SELECT x FROM cnt`,
404
+ maxRows: 10,
405
+ },
406
+ viz: { type: "table" },
407
+ },
408
+ ],
409
+ },
410
+ }),
411
+ });
412
+ expect(created.status).toBe(201);
413
+ const { id } = (await created.json()) as { id: string };
414
+
415
+ const run = await fetch(`${BASE}/api/metrics/definitions/${id}/run`, {
416
+ method: "POST",
417
+ headers,
418
+ body: JSON.stringify({ variables: {} }),
419
+ });
420
+ expect(run.status).toBe(200);
421
+ const runBody = (await run.json()) as MetricRunResponse;
422
+ expect(runBody.widgets[0]?.result.rows.length).toBe(10);
423
+ expect(runBody.widgets[0]?.result.truncated).toBe(true);
424
+ });
381
425
  });