@desplega.ai/agent-swarm 1.131.1 → 1.134.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 (360) hide show
  1. package/README.md +3 -3
  2. package/dist/{actions-jgcy477k.js → actions-7kast243.js} +20 -18
  3. package/dist/{anthropic-messages-rjmd2e7n.js → anthropic-messages-706yad38.js} +33 -17
  4. package/dist/app-0g84t8wf.js +28 -0
  5. package/dist/{artifact-jysj3wrg.js → artifact-dje5ke4h.js} +37 -131
  6. package/dist/{assistant-f2vkk1hj.js → assistant-ac4htcky.js} +25 -22
  7. package/dist/{azure-openai-responses-c0yjmbfd.js → azure-openai-responses-be59fbsz.js} +13 -13
  8. package/dist/{boot-reembed-q7mp8hcr.js → boot-reembed-b1rcqn85.js} +13 -11
  9. package/dist/{boot-reembed-xmw8wcx6.js → boot-reembed-kw2541c4.js} +14 -12
  10. package/dist/{boot-scrub-logs-dsqrv79n.js → boot-scrub-logs-ja29t92g.js} +11 -9
  11. package/dist/{claude-adapter-q6x7yspx.js → claude-adapter-5cg1fv8s.js} +33 -29
  12. package/dist/{claude-managed-adapter-wsg37bt8.js → claude-managed-adapter-9z9sdytx.js} +9 -9
  13. package/dist/{claude-managed-setup-52z4mq9w.js → claude-managed-setup-3zqp51cr.js} +5 -5
  14. package/dist/{claude-1ex232fc.js → claude-wqn3dtca.js} +1 -1
  15. package/dist/{cli-4wgngmwv.js → cli-00w4hbam.js} +1 -1
  16. package/dist/{cli-er3a4bb0.js → cli-15429qdw.js} +6 -1
  17. package/dist/{cli-e5dabkx0.js → cli-1cwhyvta.js} +4 -4
  18. package/dist/{cli-zsspvvm2.js → cli-1ybr6vw2.js} +237 -237
  19. package/dist/{cli-xpxde7rm.js → cli-2wv4kj0e.js} +60 -31
  20. package/dist/cli-34w2584r.js +530 -0
  21. package/dist/{cli-se2p6rkb.js → cli-38taxznr.js} +6 -6
  22. package/dist/{cli-9sbhdpvb.js → cli-3cgfyfzm.js} +14 -14
  23. package/dist/{cli-5n6e5yfn.js → cli-3zmdzfjf.js} +79 -74
  24. package/dist/{cli-3ty0t082.js → cli-43x36mty.js} +197 -8
  25. package/dist/{cli-ycrnynpw.js → cli-480nehnh.js} +29 -22
  26. package/dist/{cli-0258203w.js → cli-492e7ap5.js} +140 -20
  27. package/dist/{cli-3fj60etf.js → cli-497b685t.js} +3 -3
  28. package/dist/{cli-pxfxkx5m.js → cli-4bap00k0.js} +1 -1
  29. package/dist/{cli-w2wxd56v.js → cli-4bc8vmhm.js} +848 -770
  30. package/dist/{cli-aca5pz9f.js → cli-589599p9.js} +1 -1
  31. package/dist/{cli-t930g0xh.js → cli-5tnr64pm.js} +1 -1
  32. package/dist/{cli-jxg3s9ey.js → cli-65d93ves.js} +7 -7
  33. package/dist/{cli-6dpytdr2.js → cli-6x363v1z.js} +2 -2
  34. package/dist/{cli-dxak0fqd.js → cli-787ph852.js} +11 -11
  35. package/dist/{cli-793jvbsh.js → cli-7bv8nckk.js} +16 -11
  36. package/dist/{cli-zyxs93y9.js → cli-7jcc9y1r.js} +3 -3
  37. package/dist/{cli-f1am9szv.js → cli-87t2gstf.js} +1 -1
  38. package/dist/{cli-9aryf9tv.js → cli-95m51ztz.js} +1 -1
  39. package/dist/{cli-4w4fmq9x.js → cli-9w49enmh.js} +2 -2
  40. package/dist/{cli-v69vk5h0.js → cli-9y5b02vr.js} +13 -13
  41. package/dist/{cli-x23me2mg.js → cli-a1rrkrb4.js} +1 -1
  42. package/dist/{cli-psrqtw2m.js → cli-a3pkfpqs.js} +2 -2
  43. package/dist/{cli-h0rgxd24.js → cli-a4ajmfvp.js} +6 -6
  44. package/dist/{cli-ygrbmjja.js → cli-aw8bb6sf.js} +6 -6
  45. package/dist/{cli-g74vpjp2.js → cli-brm3et47.js} +55 -22
  46. package/dist/{cli-czkae1fp.js → cli-c5ddbdfj.js} +9 -9
  47. package/dist/{cli-114fvtmj.js → cli-c79a9zrc.js} +1 -1
  48. package/dist/{cli-49n9f49x.js → cli-dx3dz0ms.js} +3 -3
  49. package/dist/{cli-6dp3ydxa.js → cli-e07exmsb.js} +18 -14
  50. package/dist/cli-et1bbwqf.js +13 -0
  51. package/dist/{cli-yazx6hap.js → cli-f3s9dry0.js} +14 -14
  52. package/dist/{cli-t8d349cy.js → cli-f4wj0s3m.js} +3 -3
  53. package/dist/{cli-d6n0thxw.js → cli-f64gdfxc.js} +1 -1
  54. package/dist/{cli-pwfsqc2x.js → cli-fyd58xgb.js} +55 -55
  55. package/dist/{cli-7dm2zqy2.js → cli-g0j14czv.js} +2 -2
  56. package/dist/{cli-b0p7rfnd.js → cli-g2p9bd1z.js} +14 -14
  57. package/dist/{cli-z905tvmm.js → cli-g814jbst.js} +2 -2
  58. package/dist/{cli-5dsbcjjw.js → cli-ghexzd2d.js} +970 -970
  59. package/dist/{cli-jvv0g8bh.js → cli-gpt9kf01.js} +15 -15
  60. package/dist/{cli-rgrfzg8z.js → cli-grt6qv91.js} +380 -380
  61. package/dist/{cli-wz9115x0.js → cli-gtafvdy0.js} +8 -8
  62. package/dist/{cli-q5dzzak0.js → cli-gy07xk4s.js} +2 -2
  63. package/dist/{cli-sy7jk3tx.js → cli-h0djenzm.js} +2 -2
  64. package/dist/{cli-nrkkknat.js → cli-h37gbjbf.js} +2 -281
  65. package/dist/{cli-jevgrb4w.js → cli-h39xxnq6.js} +1200 -491
  66. package/dist/{cli-gz9sbyk3.js → cli-jepmgn7r.js} +1 -1
  67. package/dist/{cli-ccf9vy7q.js → cli-jmvk6et8.js} +7 -7
  68. package/dist/{cli-2rk0cx7s.js → cli-jxj92gkt.js} +51 -41
  69. package/dist/{cli-bqw0mt1n.js → cli-k46t4664.js} +18 -3
  70. package/dist/{cli-50vbt00w.js → cli-m07313tr.js} +1153 -696
  71. package/dist/{cli-4j2mrzag.js → cli-mkq1rk7b.js} +1 -1
  72. package/dist/{cli-1z7048dy.js → cli-mnm7w46e.js} +1 -1
  73. package/dist/{cli-kd1ck4rx.js → cli-mp9sg4cg.js} +14 -8
  74. package/dist/{cli-jbcva0xj.js → cli-mrammd8c.js} +60 -35
  75. package/dist/cli-nanaxrz6.js +27 -0
  76. package/dist/{cli-p9swy5t3.js → cli-pq2ee5ja.js} +8 -6
  77. package/dist/{cli-2wadx7p2.js → cli-pt43pdp1.js} +2 -2
  78. package/dist/{cli-7sgrxg9a.js → cli-q6d0vvey.js} +4 -4
  79. package/dist/{cli-k9qkf1hw.js → cli-qf9kpsve.js} +9 -9
  80. package/dist/cli-qh4sqfws.js +15 -0
  81. package/dist/{cli-e15jhafb.js → cli-qntkxysp.js} +14 -14
  82. package/dist/{cli-sajgpkqg.js → cli-qs9hhsvb.js} +1723 -325
  83. package/dist/{cli-z2zcxes1.js → cli-qt5bt3vk.js} +1 -1
  84. package/dist/{cli-wx82tw1k.js → cli-qtekrty1.js} +3 -3
  85. package/dist/{cli-g1re8r05.js → cli-rh6f3s9n.js} +3 -3
  86. package/dist/{cli-n0krnc6d.js → cli-rzhcrw2p.js} +2 -2
  87. package/dist/{cli-gecy3z2s.js → cli-snw62ze5.js} +4 -4
  88. package/dist/{cli-n6h8qt8n.js → cli-snz3p4yf.js} +15 -10
  89. package/dist/{cli-r6kqqbj0.js → cli-tkf8n6cw.js} +11 -11
  90. package/dist/{cli-4rkfg1wx.js → cli-tkrr24mz.js} +14 -9
  91. package/dist/{cli-9dr5qzc0.js → cli-tv9f1kyb.js} +54 -43
  92. package/dist/{cli-gm9cbhbq.js → cli-vtz6yj3v.js} +28 -9
  93. package/dist/{cli-2f07m0xr.js → cli-wf86d5t7.js} +1 -1
  94. package/dist/{cli-7zd05xb4.js → cli-whrvct19.js} +45 -45
  95. package/dist/{cli-ap1czx8f.js → cli-wrrea8xm.js} +3 -3
  96. package/dist/{cli-57khth04.js → cli-xmyqv00z.js} +120 -4
  97. package/dist/{cli-cwy5hqbq.js → cli-yjmk7tzc.js} +1 -1
  98. package/dist/{cli-6pcz9h15.js → cli-zykjrwcx.js} +180 -394
  99. package/dist/cli.js +45 -45
  100. package/dist/{codex-adapter-htjkn22b.js → codex-adapter-hsq6mz3m.js} +21 -19
  101. package/dist/{codex-hook-hq54t9ft.js → codex-hook-8c500gq8.js} +8 -7
  102. package/dist/{codex-login-z6yhbkmk.js → codex-login-x7603481.js} +3 -3
  103. package/dist/{codex-session-runner-wjfc7smz.js → codex-session-runner-c1x3x143.js} +15 -13
  104. package/dist/{commands-xhrfwqsk.js → commands-rwtddtst.js} +11 -9
  105. package/dist/{db-anvwb22f.js → db-j2b355vz.js} +458 -444
  106. package/dist/{devin-adapter-v1sarqtw.js → devin-adapter-8qmhqpy3.js} +3 -3
  107. package/dist/{devtools-2y1r7yqk.js → devtools-j2rnj8w7.js} +4 -4
  108. package/dist/{e2b-jdkwemv2.js → e2b-znpx78eg.js} +16 -16
  109. package/dist/{es5-vjgtkgp6.js → es5-fmqrjzy3.js} +2 -2
  110. package/dist/{event-bus-axn823e9.js → event-bus-n8sb35pw.js} +4 -4
  111. package/dist/{getMachineId-bsd-6tc9wxdj.js → getMachineId-bsd-wpzbhkm6.js} +4 -4
  112. package/dist/{getMachineId-darwin-sjt9sgnc.js → getMachineId-darwin-45zabjy0.js} +4 -4
  113. package/dist/{getMachineId-linux-v5n18y9p.js → getMachineId-linux-warnp5pd.js} +3 -3
  114. package/dist/{getMachineId-unsupported-yrjyqw3d.js → getMachineId-unsupported-5spent2a.js} +3 -3
  115. package/dist/{getMachineId-win-w3r8063c.js → getMachineId-win-v4bvcw7a.js} +4 -4
  116. package/dist/{google-generative-ai-hysrd8qx.js → google-generative-ai-b0wzss55.js} +17 -14
  117. package/dist/{google-vertex-p5arrga0.js → google-vertex-e8q4dpya.js} +18 -15
  118. package/dist/{handlers-b9kbc621.js → handlers-hchgrxkh.js} +32 -29
  119. package/dist/{hook-xkvn6c5h.js → hook-p49egqc0.js} +66 -52
  120. package/dist/hook-spb00w76.js +34 -0
  121. package/dist/{http-9xh6ta4r.js → http-nh14kkms.js} +663 -181
  122. package/dist/index-1qthq7vc.js +6 -0
  123. package/dist/{index-0g68f0p6.js → index-20xqmn3v.js} +3 -3
  124. package/dist/{index-vdkr5z2w.js → index-3e03k6e4.js} +114 -114
  125. package/dist/{index-ywds4812.js → index-4cbyhq2g.js} +43 -41
  126. package/dist/index-67pzqfrb.js +36 -0
  127. package/dist/{index-bccrkdkn.js → index-7m8yxkw5.js} +16 -16
  128. package/dist/{index-eaxyqcmj.js → index-dvsk64sm.js} +163 -83
  129. package/dist/{index-dqkpnmqm.js → index-e19s2m8k.js} +18 -18
  130. package/dist/{index-j30k5zxn.js → index-emvxcy98.js} +16 -16
  131. package/dist/{index-7ndw6ecy.js → index-gqnmr60d.js} +9 -9
  132. package/dist/{index-9znhar3x.js → index-j02xvjtd.js} +184 -185
  133. package/dist/{index-1s2442yw.js → index-ns5kksc1.js} +7 -7
  134. package/dist/{index-dpy2hth8.js → index-pdxc8kfh.js} +71 -71
  135. package/dist/{index-z1m7pw0v.js → index-ptcqm8bc.js} +107 -107
  136. package/dist/{index-070v4eyb.js → index-pyp7rexm.js} +5 -6
  137. package/dist/{index-86znjz1p.js → index-shne41sd.js} +13 -13
  138. package/dist/{index-2qn2qndv.js → index-t36q2v4t.js} +183 -183
  139. package/dist/index-tcnvv8w6.js +7 -0
  140. package/dist/index-w91qbfv1.js +7 -0
  141. package/dist/{index-fzc0mwvj.js → index-yqweff5n.js} +32 -32
  142. package/dist/index-yrfphye1.js +39 -0
  143. package/dist/{keepalive-vc7s2aa6.js → keepalive-9k51nqhc.js} +21 -15
  144. package/dist/lead-2xq3dxbz.js +88 -0
  145. package/dist/maintenance-0c0g6nym.js +28 -0
  146. package/dist/mistral-conversations-2njwdwhj.js +717 -0
  147. package/dist/{multipart-parser-qaktc7p8.js → multipart-parser-6gftesw0.js} +2 -2
  148. package/dist/{oauth-refresh-sweep-yecgjpk4.js → oauth-refresh-sweep-273a64r2.js} +14 -12
  149. package/dist/{onboard-xj0f73f9.js → onboard-tsv9wwhz.js} +12 -12
  150. package/dist/{openai-codex-responses-5g27pd6f.js → openai-codex-responses-kt84eq86.js} +26 -26
  151. package/dist/{openai-completions-3vkmfz2a.js → openai-completions-mrypq7qp.js} +17 -16
  152. package/dist/{openai-responses-dg4t7j7a.js → openai-responses-qhrk21ez.js} +20 -17
  153. package/dist/{opencode-adapter-1gm38aba.js → opencode-adapter-rar9676j.js} +13 -11
  154. package/dist/{openrouter-images-g3gm8931.js → openrouter-images-684fysx9.js} +5 -5
  155. package/dist/{otel-impl-nxg0n0b3.js → otel-impl-wp1mfzd2.js} +752 -537
  156. package/dist/{photon_rs-ky7dj5qw.js → photon_rs-8dwt2pzy.js} +581 -581
  157. package/dist/{pi-messages-tqj8q6b9.js → pi-messages-txp9f97r.js} +7 -7
  158. package/dist/{pi-mono-adapter-4g5kby31.js → pi-mono-adapter-yz47kk8e.js} +1910 -1180
  159. package/dist/pricing-refresh-jw1yw2fn.js +22 -0
  160. package/dist/rbac-roles-dg964c8p.js +32 -0
  161. package/dist/{rbac-roles-1hcxdtg7.js → rbac-roles-p53cg5cv.js} +19 -17
  162. package/dist/{registry-02cy9z5z.js → registry-mh12y17v.js} +4 -4
  163. package/dist/render-v2-56vn2rk9.js +36 -0
  164. package/dist/seed-pricing-52xx6z63.js +19 -0
  165. package/dist/{setup-46xtgq3c.js → setup-e8gp79sy.js} +7 -7
  166. package/dist/token-util-zy908s1a.js +6 -0
  167. package/dist/{token-fxkn2zf2.js → token-xg5711x5.js} +4 -4
  168. package/dist/worker-s17f1g46.js +88 -0
  169. package/dist/{x-6mg4kfpe.js → x-xkfh0w0v.js} +5 -5
  170. package/openapi.json +566 -22
  171. package/package.json +6 -6
  172. package/src/be/budget-refusal-notify.ts +14 -7
  173. package/src/be/db-queries/task-artifacts.ts +182 -0
  174. package/src/be/db.ts +834 -171
  175. package/src/be/events.ts +5 -0
  176. package/src/be/migrations/131_session_logs_created_at_index.sql +2 -0
  177. package/src/be/migrations/132_multi_runtime_instances.sql +44 -0
  178. package/src/be/migrations/133_workflow_run_trigger_type.sql +12 -0
  179. package/src/be/migrations/135_backfill_task_pull_request_attachments.sql +228 -0
  180. package/src/be/migrations/136_task_requester_provenance.sql +37 -0
  181. package/src/be/migrations/137_memory_retrieval_composite_index.sql +1 -0
  182. package/src/be/migrations/runner.ts +17 -11
  183. package/src/be/multi-runtime.ts +430 -0
  184. package/src/be/scripts/extract-schema.ts +3 -1
  185. package/src/be/scripts/typecheck.ts +6 -2
  186. package/src/be/seed-prompt-templates.ts +25 -2
  187. package/src/be/seed-skills/index.ts +7 -0
  188. package/src/be/steering.ts +1 -1
  189. package/src/be/swarm-config-guard.ts +18 -0
  190. package/src/commands/credential-wait.ts +54 -0
  191. package/src/commands/onboard/steps/core-credentials.tsx +1 -1
  192. package/src/commands/onboard/steps/custom-templates.tsx +1 -1
  193. package/src/commands/onboard/steps/harness-credentials.tsx +1 -1
  194. package/src/commands/onboard/steps/health-check.tsx +1 -1
  195. package/src/commands/onboard/steps/start.tsx +1 -1
  196. package/src/commands/profile-sync-audit.ts +56 -0
  197. package/src/commands/profile-sync.ts +287 -15
  198. package/src/commands/provider-credentials.ts +46 -19
  199. package/src/commands/runner.ts +199 -33
  200. package/src/commands/templates.ts +22 -0
  201. package/src/github/handlers.ts +55 -9
  202. package/src/heartbeat/heartbeat.ts +109 -17
  203. package/src/hooks/hook.ts +49 -33
  204. package/src/http/active-sessions.ts +11 -1
  205. package/src/http/agents.ts +261 -30
  206. package/src/http/all-routes.ts +1 -0
  207. package/src/http/config.ts +13 -2
  208. package/src/http/core.ts +102 -4
  209. package/src/http/db-query-bounded.ts +313 -0
  210. package/src/http/db-query-shared.ts +249 -0
  211. package/src/http/db-query.ts +54 -42
  212. package/src/http/events.ts +2 -0
  213. package/src/http/index.ts +23 -2
  214. package/src/http/mcp-bridge.ts +37 -3
  215. package/src/http/mcp.ts +12 -0
  216. package/src/http/memory.ts +13 -2
  217. package/src/http/openapi.ts +1 -0
  218. package/src/http/poll.ts +70 -27
  219. package/src/http/prompt-templates.ts +36 -13
  220. package/src/http/route-def.ts +43 -9
  221. package/src/http/schedules.ts +2 -2
  222. package/src/http/scripts.ts +10 -1
  223. package/src/http/session-data.ts +44 -0
  224. package/src/http/tasks.ts +2 -2
  225. package/src/http/users.ts +74 -52
  226. package/src/http/webhooks.ts +5 -2
  227. package/src/http/workflows.ts +1 -4
  228. package/src/integrations/kapso/inbound.ts +1 -1
  229. package/src/jira/outbound.ts +39 -8
  230. package/src/jira/webhook-lifecycle.ts +13 -2
  231. package/src/linear/outbound.ts +43 -10
  232. package/src/oauth/keepalive.ts +13 -2
  233. package/src/prompts/base-prompt.ts +1 -1
  234. package/src/prompts/default-drift.ts +21 -0
  235. package/src/prompts/session-templates.ts +56 -7
  236. package/src/providers/claude-adapter.ts +6 -0
  237. package/src/providers/codex-adapter.ts +6 -0
  238. package/src/providers/opencode-adapter.ts +6 -0
  239. package/src/providers/pi-mono-adapter.ts +8 -0
  240. package/src/providers/pricing-sources.md +24 -0
  241. package/src/queue-stall-alarm.ts +216 -0
  242. package/src/rbac/legacy-policy.ts +3 -0
  243. package/src/rbac/permissions.ts +12 -0
  244. package/src/scheduler/scheduler.ts +7 -0
  245. package/src/script-workflows/executor.ts +2 -1
  246. package/src/scripts-runtime/executors/native.ts +2 -1
  247. package/src/scripts-runtime/executors/types.ts +5 -0
  248. package/src/scripts-runtime/loader.ts +5 -0
  249. package/src/scripts-runtime/sdk-allowlist.ts +3 -0
  250. package/src/scripts-runtime/swarm-config.ts +8 -0
  251. package/src/scripts-runtime/swarm-sdk.ts +7 -0
  252. package/src/scripts-runtime/types/stdlib.d.ts +4 -0
  253. package/src/scripts-runtime/types/swarm-sdk.d.ts +4 -0
  254. package/src/server.ts +6 -0
  255. package/src/slack/channel-join.ts +22 -3
  256. package/src/slack/channel-lifecycle.ts +114 -0
  257. package/src/tests/budget-claim-gate.test.ts +20 -0
  258. package/src/tests/claude-adapter.test.ts +32 -0
  259. package/src/tests/codex-adapter.test.ts +30 -0
  260. package/src/tests/create-task-validation.test.ts +128 -0
  261. package/src/tests/credential-wait.test.ts +67 -1
  262. package/src/tests/db-query.test.ts +711 -2
  263. package/src/tests/heartbeat-checklist.test.ts +28 -0
  264. package/src/tests/http-api-integration.test.ts +81 -0
  265. package/src/tests/http-users.test.ts +166 -1
  266. package/src/tests/identity-field-budget.test.ts +176 -0
  267. package/src/tests/mcp-tools-user.test.ts +34 -0
  268. package/src/tests/mcp-user-route.test.ts +17 -0
  269. package/src/tests/metrics-http.test.ts +44 -0
  270. package/src/tests/migration-failure.test.ts +86 -0
  271. package/src/tests/multi-runtime-registration.test.ts +2810 -0
  272. package/src/tests/openapi-response-contract.test.ts +24 -0
  273. package/src/tests/openapi-route-headers.test.ts +83 -0
  274. package/src/tests/opencode-adapter.test.ts +40 -0
  275. package/src/tests/outbound-listener-rejection.test.ts +138 -0
  276. package/src/tests/pi-mono-adapter.test.ts +98 -0
  277. package/src/tests/preload.ts +2 -0
  278. package/src/tests/profile-sync-warning.test.ts +277 -0
  279. package/src/tests/profile-sync.test.ts +232 -6
  280. package/src/tests/prompt-template-resolver.test.ts +177 -6
  281. package/src/tests/prompt-template-session.test.ts +39 -3
  282. package/src/tests/queue-stall-alarm.test.ts +226 -0
  283. package/src/tests/rbac-charact-slack.test.ts +29 -0
  284. package/src/tests/rbac-engine.test.ts +3 -0
  285. package/src/tests/requester-comms.test.ts +32 -0
  286. package/src/tests/runner-requester-profile.test.ts +38 -0
  287. package/src/tests/schedule-target-type.test.ts +38 -0
  288. package/src/tests/script-connections.test.ts +13 -1
  289. package/src/tests/scripts-runtime-identity.test.ts +375 -0
  290. package/src/tests/scripts-runtime.test.ts +4 -3
  291. package/src/tests/sdk-allowlist.test.ts +47 -0
  292. package/src/tests/send-task-requested-by.test.ts +9 -0
  293. package/src/tests/session-costs.test.ts +506 -1
  294. package/src/tests/shutdown-error-scrubbing.test.ts +92 -0
  295. package/src/tests/slack-channel-lifecycle.test.ts +241 -0
  296. package/src/tests/slack-render-v2.test.ts +43 -0
  297. package/src/tests/steering-core.test.ts +28 -0
  298. package/src/tests/swarm-config-reserved-keys.test.ts +22 -0
  299. package/src/tests/task-artifacts.test.ts +386 -0
  300. package/src/tests/task-attachments-schema.test.ts +17 -1
  301. package/src/tests/task-completion-idempotency.test.ts +57 -0
  302. package/src/tests/task-pull-request-backfill-migration.test.ts +321 -0
  303. package/src/tests/tool-annotations.test.ts +4 -0
  304. package/src/tests/update-profile-api.test.ts +67 -39
  305. package/src/tests/update-profile-auth.test.ts +48 -4
  306. package/src/tests/user-attribution-migration.test.ts +54 -0
  307. package/src/tests/user-identity.test.ts +72 -0
  308. package/src/tests/workflow-async-v2.test.ts +39 -1
  309. package/src/tests/workflow-http-v2.test.ts +3 -2
  310. package/src/tests/workflow-mcp-trigger-schema.test.ts +52 -3
  311. package/src/tests/workflow-retry-v2.test.ts +45 -1
  312. package/src/tools/db-query.ts +18 -8
  313. package/src/tools/get-task-details.ts +24 -3
  314. package/src/tools/manage-user.ts +36 -3
  315. package/src/tools/poll-task.ts +93 -14
  316. package/src/tools/script-common.ts +5 -0
  317. package/src/tools/send-task.ts +1 -4
  318. package/src/tools/slack-archive-channel.ts +70 -0
  319. package/src/tools/slack-create-channel.ts +74 -0
  320. package/src/tools/slack-invite-to-channel.ts +78 -0
  321. package/src/tools/store-progress.ts +13 -2
  322. package/src/tools/swarm-config/delete-config.ts +9 -0
  323. package/src/tools/swarm-config/set-config.ts +3 -2
  324. package/src/tools/task-action.ts +30 -0
  325. package/src/tools/task-tool-ctx.ts +8 -1
  326. package/src/tools/tool-config.ts +4 -1
  327. package/src/tools/update-profile.ts +16 -21
  328. package/src/tools/utils.ts +5 -0
  329. package/src/tools/workflows/trigger-workflow.ts +7 -1
  330. package/src/types.ts +165 -1
  331. package/src/utils/github-pull-request.ts +76 -0
  332. package/src/utils/identity-field-budget.ts +73 -0
  333. package/src/utils/multi-runtime.ts +20 -0
  334. package/src/utils/requester-comms.ts +24 -0
  335. package/src/workflows/engine.ts +22 -2
  336. package/src/workflows/resume.ts +19 -11
  337. package/src/workflows/retry-poller.ts +1 -0
  338. package/src/workflows/triggers.ts +4 -0
  339. package/templates/skills/db-query-guidance/config.json +14 -0
  340. package/templates/skills/db-query-guidance/content.md +57 -0
  341. package/dist/app-5da78bd4.js +0 -26
  342. package/dist/cli-3te4jdkm.js +0 -225
  343. package/dist/cli-bmze986d.js +0 -7
  344. package/dist/cli-kh3ba2zf.js +0 -607
  345. package/dist/hook-cgk4wxmx.js +0 -31
  346. package/dist/index-25wjsdv2.js +0 -34
  347. package/dist/index-76at2ymp.js +0 -7
  348. package/dist/index-hase6zbe.js +0 -6
  349. package/dist/index-vg4snay4.js +0 -36
  350. package/dist/index-vy8edt27.js +0 -7
  351. package/dist/lead-r2yqfmx7.js +0 -85
  352. package/dist/maintenance-mw70k468.js +0 -26
  353. package/dist/mistral-conversations-81fwav0j.js +0 -30578
  354. package/dist/otel-nqa12wf3.js +0 -3149
  355. package/dist/pricing-refresh-2cn6effr.js +0 -20
  356. package/dist/rbac-roles-vynday5g.js +0 -30
  357. package/dist/render-v2-tc0bh8kk.js +0 -34
  358. package/dist/seed-pricing-8kceb43f.js +0 -17
  359. package/dist/token-util-pqyc8kng.js +0 -6
  360. package/dist/worker-qrbz6hmv.js +0 -85
package/src/be/db.ts CHANGED
@@ -116,19 +116,28 @@ import type {
116
116
  } from "../types";
117
117
  import {
118
118
  AgentAvatarSchema,
119
+ type CreateTaskOptions,
120
+ CreateTaskOptionsSchema,
119
121
  FollowUpConfigSchema,
120
122
  isTerminalTaskStatus,
121
123
  type ModelTier,
122
124
  parseModelTier,
123
125
  ReasoningEffortSchema,
124
126
  RoutingAffinitySchema,
127
+ SERVER_GENERATED_ATTACHMENT_CAPABILITY,
125
128
  SessionCostModelBreakdownSchema,
126
129
  } from "../types";
127
130
  import { deriveProviderFromKeyType } from "../utils/credentials";
128
131
  import { isEnvFlagEnabled } from "../utils/env-flag";
129
132
  import type { RateLimitWindowTelemetry } from "../utils/error-tracker";
133
+ import { extractGitHubPullRequestUrls } from "../utils/github-pull-request";
134
+ import {
135
+ type BudgetedIdentityField,
136
+ checkIdentityFieldBudget,
137
+ IdentityFieldBudgetError,
138
+ } from "../utils/identity-field-budget";
130
139
  import { getCurrentRequestUserId } from "../utils/request-auth-context";
131
- import { scrubSecrets } from "../utils/secret-scrubber";
140
+ import { registerVolatileSecret, scrubSecrets } from "../utils/secret-scrubber";
132
141
  import { auditAssetKeys, enforceAssetKeyStartupAudit } from "./asset-key-audit";
133
142
  import { migrateLegacyCredentialBindingBlob } from "./connection-bindings-blob-migration";
134
143
  import { decryptSecret, encryptSecret, getEncryptionKey, resolveEncryptionKey } from "./crypto";
@@ -181,6 +190,40 @@ export function isSqliteVecAvailable(): boolean {
181
190
  return sqliteVecAvailable;
182
191
  }
183
192
 
193
+ /**
194
+ * Resolve the sqlite-vec loadable extension path without opening a Database.
195
+ * Mirrors loadSqliteVec's env-var-first, npm-fallback resolution, but returns
196
+ * the path instead of loading it: the bounded db-query child process (see
197
+ * src/http/db-query-bounded.ts) opens its own read-only connection and must
198
+ * load the extension itself, so the parent resolves the path once and passes
199
+ * it along. The result is memoized — `undefined` is a valid resolved answer
200
+ * (extension genuinely unavailable) and is cached too, so a failing
201
+ * `require` doesn't retry on every query.
202
+ */
203
+ let cachedSqliteVecExtensionPath: string | undefined | null = null;
204
+
205
+ export function resolveSqliteVecExtensionPath(): string | undefined {
206
+ if (cachedSqliteVecExtensionPath !== null) return cachedSqliteVecExtensionPath;
207
+ const extensionPath = process.env.SQLITE_VEC_EXTENSION_PATH;
208
+ if (extensionPath) {
209
+ cachedSqliteVecExtensionPath = extensionPath;
210
+ return cachedSqliteVecExtensionPath;
211
+ }
212
+ try {
213
+ cachedSqliteVecExtensionPath = (
214
+ require("sqlite-vec") as { getLoadablePath(): string }
215
+ ).getLoadablePath();
216
+ } catch {
217
+ cachedSqliteVecExtensionPath = undefined;
218
+ }
219
+ return cachedSqliteVecExtensionPath;
220
+ }
221
+
222
+ /** Test-only: clear the memoized sqlite-vec extension path cache. */
223
+ export function __resetSqliteVecExtensionPathCacheForTests(): void {
224
+ cachedSqliteVecExtensionPath = null;
225
+ }
226
+
184
227
  function loadSqliteVec(database: Database): void {
185
228
  sqliteVecAvailable = false;
186
229
  try {
@@ -247,7 +290,17 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
247
290
  loadSqliteVec(database);
248
291
 
249
292
  // Run database migrations (schema creation + incremental changes)
250
- runMigrations(database);
293
+ try {
294
+ runMigrations(database);
295
+ } catch (error) {
296
+ db = null;
297
+ try {
298
+ database.close();
299
+ } catch (closeError) {
300
+ console.error("[migrations] Failed to close database after migration failure:", closeError);
301
+ }
302
+ throw error;
303
+ }
251
304
 
252
305
  // Compatibility migration for legacy databases that predate profile fields
253
306
  ensureAgentProfileColumns(database);
@@ -432,6 +485,13 @@ const VERSIONABLE_FIELDS: VersionableField[] = [
432
485
  "heartbeatMd",
433
486
  ];
434
487
 
488
+ const BUDGETED_IDENTITY_FIELDS: BudgetedIdentityField[] = [
489
+ "soulMd",
490
+ "identityMd",
491
+ "claudeMd",
492
+ "toolsMd",
493
+ ];
494
+
435
495
  function ensureAgentProfileColumns(database: Database): void {
436
496
  const existingColumns = new Set(
437
497
  database
@@ -775,6 +835,19 @@ export function updateAgentCredentialState(
775
835
  return row ? rowToAgent(row) : null;
776
836
  }
777
837
 
838
+ /**
839
+ * Record which env vars a worker is missing without touching status — the
840
+ * logical status is derived from runtime readiness in multi-runtime mode.
841
+ */
842
+ export function updateAgentCredentialMissing(agentId: string, missing: string[] | null): void {
843
+ const json = missing && missing.length > 0 ? JSON.stringify(missing) : null;
844
+ getDb()
845
+ .prepare(
846
+ "UPDATE agents SET credentialMissing = ?, lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?",
847
+ )
848
+ .run(json, agentId);
849
+ }
850
+
778
851
  export function createAgent(
779
852
  agent: Omit<Agent, "id" | "createdAt" | "lastUpdatedAt"> & { id?: string },
780
853
  ): Agent {
@@ -1014,12 +1087,22 @@ export function deleteAgent(id: string): boolean {
1014
1087
  * Get the count of active (in_progress) tasks for an agent.
1015
1088
  * Used to determine current capacity usage.
1016
1089
  */
1090
+ /**
1091
+ * Tasks occupying one of the agent's concurrency slots.
1092
+ *
1093
+ * A claimed offer counts too — omitting it let a second concurrent poll take
1094
+ * another task past the limit. It is counted only through `offeredTo`, the
1095
+ * agent actually reviewing it: `agentId` on an offer may still be the lead
1096
+ * that created it, which would otherwise consume the lead's own capacity.
1097
+ */
1017
1098
  export function getActiveTaskCount(agentId: string): number {
1018
1099
  const result = getDb()
1019
- .prepare<{ count: number }, [string]>(
1020
- "SELECT COUNT(*) as count FROM agent_tasks WHERE agentId = ? AND status = 'in_progress'",
1100
+ .prepare<{ count: number }, [string, string]>(
1101
+ `SELECT COUNT(*) as count FROM agent_tasks
1102
+ WHERE (agentId = ? AND status = 'in_progress')
1103
+ OR (offeredTo = ? AND status = 'reviewing')`,
1021
1104
  )
1022
- .get(agentId);
1105
+ .get(agentId, agentId);
1023
1106
  return result?.count ?? 0;
1024
1107
  }
1025
1108
 
@@ -1202,6 +1285,7 @@ type AgentTaskRow = {
1202
1285
  credentialKeySuffix: string | null;
1203
1286
  credentialKeyType: string | null;
1204
1287
  requestedByUserId: string | null;
1288
+ requestedByUserIdInherited: number;
1205
1289
  swarmVersion: string | null;
1206
1290
  provider: string | null;
1207
1291
  providerMeta: string | null;
@@ -2143,13 +2227,29 @@ export function updateTaskVcs(
2143
2227
  vcsUrl: string;
2144
2228
  },
2145
2229
  ): AgentTask | null {
2146
- const row = getDb()
2147
- .prepare<AgentTaskRow, [string, string, number, string, string, string]>(
2148
- `UPDATE agent_tasks
2149
- SET vcsProvider = ?, vcsRepo = ?, vcsNumber = ?, vcsUrl = ?, lastUpdatedAt = ?
2150
- WHERE id = ? RETURNING *`,
2151
- )
2152
- .get(vcs.vcsProvider, vcs.vcsRepo, vcs.vcsNumber, vcs.vcsUrl, new Date().toISOString(), taskId);
2230
+ const row = getDb().transaction(() => {
2231
+ const updated = getDb()
2232
+ .prepare<AgentTaskRow, [string, string, number, string, string, string]>(
2233
+ `UPDATE agent_tasks
2234
+ SET vcsProvider = ?, vcsRepo = ?, vcsNumber = ?, vcsUrl = ?, lastUpdatedAt = ?
2235
+ WHERE id = ? RETURNING *`,
2236
+ )
2237
+ .get(
2238
+ vcs.vcsProvider,
2239
+ vcs.vcsRepo,
2240
+ vcs.vcsNumber,
2241
+ vcs.vcsUrl,
2242
+ new Date().toISOString(),
2243
+ taskId,
2244
+ );
2245
+ if (updated) {
2246
+ reconcileTaskPullRequestAttachments(taskId, updated.agentId, [
2247
+ updated.output,
2248
+ vcs.vcsProvider === "github" ? vcs.vcsUrl : null,
2249
+ ]);
2250
+ }
2251
+ return updated;
2252
+ })();
2153
2253
  return row ? rowToAgentTask(row) : null;
2154
2254
  }
2155
2255
 
@@ -2931,13 +3031,23 @@ export function completeTask(id: string, output?: string): AgentTask | null {
2931
3031
  return null;
2932
3032
  }
2933
3033
 
2934
- const finishedAt = new Date().toISOString();
2935
- let row = taskQueries.updateStatus().get("completed", finishedAt, id);
2936
- if (!row) return null;
3034
+ const row = getDb().transaction(() => {
3035
+ const finishedAt = new Date().toISOString();
3036
+ let completed = taskQueries.updateStatus().get("completed", finishedAt, id);
3037
+ if (!completed) return null;
2937
3038
 
2938
- if (output) {
2939
- row = taskQueries.setOutput().get(scrubSecrets(output), id);
2940
- }
3039
+ if (output) {
3040
+ completed = taskQueries.setOutput().get(scrubSecrets(output), id);
3041
+ }
3042
+ if (completed) {
3043
+ reconcileTaskPullRequestAttachments(id, completed.agentId, [
3044
+ completed.output,
3045
+ completed.vcsProvider === "github" ? completed.vcsUrl : null,
3046
+ ]);
3047
+ }
3048
+ return completed;
3049
+ })();
3050
+ if (!row) return null;
2941
3051
 
2942
3052
  if (row && oldTask) {
2943
3053
  emitTaskLifecycleTelemetryAfterCommit(
@@ -2961,8 +3071,8 @@ export function completeTask(id: string, output?: string): AgentTask | null {
2961
3071
  newValue: "completed",
2962
3072
  });
2963
3073
  } catch {}
2964
- try {
2965
- import("../workflows/event-bus").then(({ workflowEventBus }) => {
3074
+ import("../workflows/event-bus")
3075
+ .then(({ workflowEventBus }) => {
2966
3076
  workflowEventBus.emit("task.completed", {
2967
3077
  taskId: id,
2968
3078
  output,
@@ -2970,8 +3080,13 @@ export function completeTask(id: string, output?: string): AgentTask | null {
2970
3080
  workflowRunId: row.workflowRunId,
2971
3081
  workflowRunStepId: row.workflowRunStepId,
2972
3082
  });
2973
- });
2974
- } catch {}
3083
+ })
3084
+ .catch((err) =>
3085
+ console.error(
3086
+ "[db] task.completed event not emitted:",
3087
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
3088
+ ),
3089
+ );
2975
3090
  try {
2976
3091
  promotePendingSteeringForTask(id, "Task completed before steering was delivered");
2977
3092
  } catch (error) {
@@ -3022,8 +3137,8 @@ export function failTask(id: string, reason: string): AgentTask | null {
3022
3137
  metadata: { reason: scrubbedReason },
3023
3138
  });
3024
3139
  } catch {}
3025
- try {
3026
- import("../workflows/event-bus").then(({ workflowEventBus }) => {
3140
+ import("../workflows/event-bus")
3141
+ .then(({ workflowEventBus }) => {
3027
3142
  workflowEventBus.emit("task.failed", {
3028
3143
  taskId: id,
3029
3144
  failureReason: reason,
@@ -3031,8 +3146,13 @@ export function failTask(id: string, reason: string): AgentTask | null {
3031
3146
  workflowRunId: row.workflowRunId,
3032
3147
  workflowRunStepId: row.workflowRunStepId,
3033
3148
  });
3034
- });
3035
- } catch {}
3149
+ })
3150
+ .catch((err) =>
3151
+ console.error(
3152
+ "[db] task.failed event not emitted:",
3153
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
3154
+ ),
3155
+ );
3036
3156
  try {
3037
3157
  promotePendingSteeringForTask(id, "Task failed before steering was delivered");
3038
3158
  } catch (error) {
@@ -3070,7 +3190,16 @@ export function overwriteTerminalTaskResultText(
3070
3190
  patch.failureReason !== undefined
3071
3191
  ? scrubSecrets(patch.failureReason)
3072
3192
  : (task.failureReason ?? null);
3073
- const row = taskQueries.setTerminalResultText().get(output, failureReason, id) ?? null;
3193
+ const row = getDb().transaction(() => {
3194
+ const updated = taskQueries.setTerminalResultText().get(output, failureReason, id) ?? null;
3195
+ if (updated && patch.output !== undefined) {
3196
+ reconcileTaskPullRequestAttachments(id, updated.agentId, [
3197
+ updated.output,
3198
+ updated.vcsProvider === "github" ? updated.vcsUrl : null,
3199
+ ]);
3200
+ }
3201
+ return updated;
3202
+ })();
3074
3203
 
3075
3204
  return row ? rowToAgentTask(row) : task;
3076
3205
  }
@@ -3113,16 +3242,21 @@ export function cancelTask(id: string, reason?: string): AgentTask | null {
3113
3242
  metadata: reason ? { reason } : undefined,
3114
3243
  });
3115
3244
  } catch {}
3116
- try {
3117
- import("../workflows/event-bus").then(({ workflowEventBus }) => {
3245
+ import("../workflows/event-bus")
3246
+ .then(({ workflowEventBus }) => {
3118
3247
  workflowEventBus.emit("task.cancelled", {
3119
3248
  taskId: id,
3120
3249
  agentId: row.agentId,
3121
3250
  workflowRunId: row.workflowRunId,
3122
3251
  workflowRunStepId: row.workflowRunStepId,
3123
3252
  });
3124
- });
3125
- } catch {}
3253
+ })
3254
+ .catch((err) =>
3255
+ console.error(
3256
+ "[db] task.cancelled event not emitted:",
3257
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
3258
+ ),
3259
+ );
3126
3260
  try {
3127
3261
  promotePendingSteeringForTask(id, "Task was cancelled before steering was delivered");
3128
3262
  } catch (error) {
@@ -3203,8 +3337,8 @@ export function supersedeTask(
3203
3337
  metadata: { reason: args.reason, resumeTaskId: args.resumeTaskId },
3204
3338
  });
3205
3339
  } catch {}
3206
- try {
3207
- import("../workflows/event-bus").then(({ workflowEventBus }) => {
3340
+ import("../workflows/event-bus")
3341
+ .then(({ workflowEventBus }) => {
3208
3342
  workflowEventBus.emit("task.superseded", {
3209
3343
  taskId: id,
3210
3344
  reason: args.reason,
@@ -3213,8 +3347,13 @@ export function supersedeTask(
3213
3347
  workflowRunId: row.workflowRunId,
3214
3348
  workflowRunStepId: row.workflowRunStepId,
3215
3349
  });
3216
- });
3217
- } catch {}
3350
+ })
3351
+ .catch((err) =>
3352
+ console.error(
3353
+ "[db] task.superseded event not emitted:",
3354
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
3355
+ ),
3356
+ );
3218
3357
 
3219
3358
  try {
3220
3359
  cascadeFailDependents(id, "superseded");
@@ -3443,15 +3582,20 @@ export function updateTaskProgress(id: string, progress: string): AgentTask | nu
3443
3582
  newValue: scrubbedProgress,
3444
3583
  });
3445
3584
  } catch {}
3446
- try {
3447
- import("../workflows/event-bus").then(({ workflowEventBus }) => {
3585
+ import("../workflows/event-bus")
3586
+ .then(({ workflowEventBus }) => {
3448
3587
  workflowEventBus.emit("task.progress", {
3449
3588
  taskId: id,
3450
3589
  progress: scrubbedProgress,
3451
3590
  agentId: row.agentId,
3452
3591
  });
3453
- });
3454
- } catch {}
3592
+ })
3593
+ .catch((err) =>
3594
+ console.error(
3595
+ "[db] task.progress event not emitted:",
3596
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
3597
+ ),
3598
+ );
3455
3599
  }
3456
3600
  return row ? rowToAgentTask(row) : null;
3457
3601
  }
@@ -3592,6 +3736,101 @@ export function insertTaskAttachment(input: InsertTaskAttachmentInput): TaskAtta
3592
3736
  })();
3593
3737
  }
3594
3738
 
3739
+ const GENERATED_PULL_REQUEST_PROVIDER_ID = "github";
3740
+ const GENERATED_PULL_REQUEST_INTENT = "task-deliverable";
3741
+ const GENERATED_PULL_REQUEST_DESCRIPTION = "Pull request shipped by this task";
3742
+ const GENERATED_PULL_REQUEST_SOURCE = "task-pull-request-recorder";
3743
+
3744
+ function pullRequestKey(pullRequest: { owner: string; repo: string; number: number }): string {
3745
+ return `${pullRequest.owner.toLowerCase()}/${pullRequest.repo.toLowerCase()}#${pullRequest.number}`;
3746
+ }
3747
+
3748
+ /**
3749
+ * Persist PRs detected by server-owned task lifecycle paths. Existing URL
3750
+ * attachments win regardless of display name, so an agent-authored row and an
3751
+ * automatic row never duplicate the same task deliverable. A capability key
3752
+ * rejected by AttachmentInputSchema marks server-generated rows so
3753
+ * reconciliation cannot mistake caller-authored metadata for provenance.
3754
+ */
3755
+ export function recordTaskPullRequestAttachments(
3756
+ taskId: string,
3757
+ agentId: string | null,
3758
+ text: string | null | undefined,
3759
+ ): TaskAttachment[] {
3760
+ const pullRequests = extractGitHubPullRequestUrls(text);
3761
+ if (pullRequests.length === 0) return [];
3762
+
3763
+ const existingPullRequests = new Set(
3764
+ getDb()
3765
+ .prepare<{ url: string }, [string]>(
3766
+ "SELECT url FROM task_attachments WHERE task_id = ? AND kind = 'url' AND url IS NOT NULL",
3767
+ )
3768
+ .all(taskId)
3769
+ .flatMap((row) => extractGitHubPullRequestUrls(row.url))
3770
+ .map(pullRequestKey),
3771
+ );
3772
+ const stored: TaskAttachment[] = [];
3773
+ for (const pullRequest of pullRequests) {
3774
+ const dedupeKey = pullRequestKey(pullRequest);
3775
+ if (existingPullRequests.has(dedupeKey)) continue;
3776
+ stored.push(
3777
+ insertTaskAttachment({
3778
+ taskId,
3779
+ agentId,
3780
+ name: `GitHub pull request #${pullRequest.number}`,
3781
+ kind: "url",
3782
+ url: pullRequest.url,
3783
+ providerId: GENERATED_PULL_REQUEST_PROVIDER_ID,
3784
+ capabilities: {
3785
+ [SERVER_GENERATED_ATTACHMENT_CAPABILITY]: GENERATED_PULL_REQUEST_SOURCE,
3786
+ },
3787
+ intent: GENERATED_PULL_REQUEST_INTENT,
3788
+ description: GENERATED_PULL_REQUEST_DESCRIPTION,
3789
+ }),
3790
+ );
3791
+ existingPullRequests.add(dedupeKey);
3792
+ }
3793
+ return stored;
3794
+ }
3795
+
3796
+ /** Reconcile only lifecycle-generated PR rows against their current source text. */
3797
+ function reconcileTaskPullRequestAttachments(
3798
+ taskId: string,
3799
+ agentId: string | null,
3800
+ sourceTexts: Array<string | null | undefined>,
3801
+ ): TaskAttachment[] {
3802
+ const desiredPullRequests = sourceTexts.flatMap(extractGitHubPullRequestUrls);
3803
+ const desiredKeys = new Set(desiredPullRequests.map(pullRequestKey));
3804
+ const generatedRows = getDb()
3805
+ .prepare<{ id: string; name: string; url: string }, [string, string]>(
3806
+ `SELECT id, name, url
3807
+ FROM task_attachments
3808
+ WHERE task_id = ?
3809
+ AND kind = 'url'
3810
+ AND url IS NOT NULL
3811
+ AND json_extract(capabilities, '$.${SERVER_GENERATED_ATTACHMENT_CAPABILITY}') = ?`,
3812
+ )
3813
+ .all(taskId, GENERATED_PULL_REQUEST_SOURCE);
3814
+
3815
+ for (const row of generatedRows) {
3816
+ const pullRequest = extractGitHubPullRequestUrls(row.url)[0];
3817
+ const expectedName = pullRequest ? `GitHub pull request #${pullRequest.number}` : null;
3818
+ if (
3819
+ !pullRequest ||
3820
+ row.name !== expectedName ||
3821
+ !desiredKeys.has(pullRequestKey(pullRequest))
3822
+ ) {
3823
+ getDb().run("DELETE FROM task_attachments WHERE id = ?", [row.id]);
3824
+ }
3825
+ }
3826
+
3827
+ return recordTaskPullRequestAttachments(
3828
+ taskId,
3829
+ agentId,
3830
+ desiredPullRequests.map((pullRequest) => pullRequest.url).join("\n"),
3831
+ );
3832
+ }
3833
+
3595
3834
  function insertTaskAttachmentRow(input: InsertTaskAttachmentInput): TaskAttachment {
3596
3835
  const db = getDb();
3597
3836
 
@@ -4638,89 +4877,11 @@ export function getAllLogs(limit?: number): AgentLog[] {
4638
4877
  // Task Pool Operations
4639
4878
  // ============================================================================
4640
4879
 
4641
- export interface CreateTaskOptions {
4642
- key?: string;
4643
- agentId?: string | null;
4644
- creatorAgentId?: string;
4645
- source?: AgentTaskSource;
4646
- taskType?: string;
4647
- tags?: string[];
4648
- priority?: number;
4649
- dependsOn?: string[];
4650
- offeredTo?: string;
4651
- status?: "backlog" | "unassigned"; // Explicitly set initial status
4652
- slackChannelId?: string;
4653
- slackThreadTs?: string;
4654
- /** Exact Slack message that directly triggered this task; never inherited. */
4655
- slackTriggerMessageTs?: string;
4656
- slackUserId?: string;
4657
- /**
4658
- * Opt out of the residual Slack/contextKey normalization below (see the
4659
- * "Residual-mismatch guard" comment near the INSERT): a deliberate
4660
- * cross-channel/thread dispatch (e.g. `send-task`'s
4661
- * `overrideSlackContext: true`) sets this so its explicit slackChannelId/
4662
- * slackThreadTs survive even when they disagree with a slack-family
4663
- * `contextKey`/parent. Trusted callers that don't set this get normalized —
4664
- * this boundary must not let a caller silently persist a mismatch.
4665
- */
4666
- overrideSlackContext?: boolean;
4667
- vcsProvider?: "github" | "gitlab";
4668
- vcsRepo?: string;
4669
- vcsEventType?: string;
4670
- vcsNumber?: number;
4671
- vcsCommentId?: number;
4672
- vcsAuthor?: string;
4673
- vcsUrl?: string;
4674
- vcsInstallationId?: number;
4675
- vcsNodeId?: string;
4676
- agentmailInboxId?: string;
4677
- agentmailMessageId?: string;
4678
- agentmailThreadId?: string;
4679
- mentionMessageId?: string;
4680
- mentionChannelId?: string;
4681
- dir?: string;
4682
- parentTaskId?: string;
4683
- model?: string;
4684
- modelTier?: ModelTier;
4685
- effort?: ReasoningEffort;
4686
- scheduleId?: string;
4687
- workflowRunId?: string;
4688
- workflowRunStepId?: string;
4689
- sourceTaskId?: string;
4690
- /**
4691
- * Optional JSON Schema the agent's final output must conform to.
4692
- *
4693
- * Enforced via the MCP `store-progress` tool (validated in
4694
- * `src/tools/store-progress.ts`). NOT enforced when the task runs on
4695
- * default-mode Devin (no MCP) — see runbooks/harness-providers.md
4696
- * ("Per-task outputSchema support"). Callers reading `task.output` for
4697
- * a schema'd task should be defensive about JSON parsing.
4698
- */
4699
- outputSchema?: Record<string, unknown>;
4700
- /**
4701
- * When a `parentTaskId` is set, the child inherits the parent's `outputSchema`
4702
- * by default. Set this to `false` to opt out — used by control-plane children
4703
- * (e.g. the Lead `reroute-decision` task) that must inherit Slack/VCS context
4704
- * from the parent but must NOT be forced to satisfy the original work's output
4705
- * contract on completion (which would block the control task — DES-523).
4706
- */
4707
- inheritParentOutputSchema?: boolean;
4708
- followUpConfig?: FollowUpConfig;
4709
- requestedByUserId?: string;
4710
- contextKey?: string;
4711
- /**
4712
- * Internal control-plane escape hatch for continuations that MUST coexist
4713
- * with their active Linear parent (for example promoted steering). General
4714
- * task creation keeps tracker-context dedup enabled.
4715
- */
4716
- bypassTrackerContextDedup?: boolean;
4717
- /**
4718
- * Routing-affinity snapshot gating pool eligibility (see
4719
- * `isAgentEligibleForTask`). Inherited from the parent (via `parentTaskId`)
4720
- * when not explicitly set — same treatment as `vcsRepo`/`contextKey`.
4721
- */
4722
- routingAffinity?: RoutingAffinity;
4723
- }
4880
+ // The runtime contract for task creation lives in src/types.ts
4881
+ // (`CreateTaskOptionsSchema`); `createTaskExtended` parses options against it.
4882
+ // The type is re-exported so existing importers (src/tasks/sibling-awareness.ts)
4883
+ // keep resolving it from this module.
4884
+ export type { CreateTaskOptions } from "../types";
4724
4885
 
4725
4886
  /**
4726
4887
  * Find recent tasks within a time window for deduplication checks.
@@ -4759,6 +4920,15 @@ export function findRecentSimilarTasks(opts: {
4759
4920
  }
4760
4921
 
4761
4922
  export function createTaskExtended(task: string, options?: CreateTaskOptions): AgentTask {
4923
+ if (typeof task !== "string" || task.trim().length === 0) {
4924
+ throw new Error("createTaskExtended: 'task' must be a non-empty string");
4925
+ }
4926
+ // Single runtime enforcement point for every task write (REST, MCP,
4927
+ // scripts bridge, webhooks, scheduler, internal callers). Reject, never
4928
+ // coerce: a bad shape throws before anything reaches the INSERT; absent
4929
+ // fields keep the `??` defaults at the bind site below.
4930
+ options = CreateTaskOptionsSchema.parse(options ?? {});
4931
+ let requestedByUserIdInherited = false;
4762
4932
  const id = crypto.randomUUID();
4763
4933
  const now = new Date().toISOString();
4764
4934
  const status: AgentTaskStatus = options?.offeredTo
@@ -4894,6 +5064,7 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
4894
5064
  // Attribution
4895
5065
  if (parent.requestedByUserId && !options.requestedByUserId) {
4896
5066
  options.requestedByUserId = parent.requestedByUserId;
5067
+ requestedByUserIdInherited = true;
4897
5068
  }
4898
5069
  if (parent.key && !options.key) {
4899
5070
  options.key = parent.key;
@@ -5009,8 +5180,8 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
5009
5180
  vcsInstallationId, vcsNodeId,
5010
5181
  agentmailInboxId, agentmailMessageId, agentmailThreadId,
5011
5182
  mentionMessageId, mentionChannelId, dir, parentTaskId, model, modelTier, effort, scheduleId,
5012
- workflowRunId, workflowRunStepId, outputSchema, followUpConfig, requestedByUserId, contextKey, routingAffinity, swarmVersion, createdAt, lastUpdatedAt, created_by, updated_by
5013
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
5183
+ workflowRunId, workflowRunStepId, outputSchema, followUpConfig, requestedByUserId, requestedByUserIdInherited, contextKey, routingAffinity, swarmVersion, createdAt, lastUpdatedAt, created_by, updated_by
5184
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
5014
5185
  )
5015
5186
  .get(
5016
5187
  id,
@@ -5055,6 +5226,7 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
5055
5226
  options?.outputSchema ? JSON.stringify(options.outputSchema) : null,
5056
5227
  options?.followUpConfig ? JSON.stringify(options.followUpConfig) : null,
5057
5228
  options?.requestedByUserId ?? null,
5229
+ requestedByUserIdInherited ? 1 : 0,
5058
5230
  options?.contextKey ?? null,
5059
5231
  options?.routingAffinity ? JSON.stringify(options.routingAffinity) : null,
5060
5232
  pkg.version,
@@ -5088,8 +5260,8 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
5088
5260
  (task) => task !== null,
5089
5261
  );
5090
5262
 
5091
- try {
5092
- import("../workflows/event-bus").then(({ workflowEventBus }) => {
5263
+ import("../workflows/event-bus")
5264
+ .then(({ workflowEventBus }) => {
5093
5265
  workflowEventBus.emit("task.created", {
5094
5266
  taskId: row.id,
5095
5267
  task: row.task,
@@ -5099,8 +5271,13 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
5099
5271
  workflowRunId: row.workflowRunId,
5100
5272
  workflowRunStepId: row.workflowRunStepId,
5101
5273
  });
5102
- });
5103
- } catch {}
5274
+ })
5275
+ .catch((err) =>
5276
+ console.error(
5277
+ "[db] task.created event not emitted:",
5278
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
5279
+ ),
5280
+ );
5104
5281
 
5105
5282
  return rowToAgentTask(row);
5106
5283
  }
@@ -5330,6 +5507,41 @@ export function releaseStaleReviewingTasks(timeoutMinutes: number = 30): number
5330
5507
  return result.changes;
5331
5508
  }
5332
5509
 
5510
+ /**
5511
+ * Release 'offered' tasks whose offeree can no longer possibly poll for them
5512
+ * — the agent was deleted, or is `offline` (multi-runtime expiry once its
5513
+ * last runtime instance ages out, or an explicit graceful close). Returns
5514
+ * them to `unassigned` rather than back to `offered`: a stuck offer is
5515
+ * invisible to `autoAssignPoolTasks` (which only scans `unassigned`) and to
5516
+ * the poll-based accept/reject path (which only the original offeree can
5517
+ * reach), so `offered` is a dead end once the offeree is gone (#1190).
5518
+ *
5519
+ * Deliberately gated on liveness, not elapsed time: a fixed timeout would
5520
+ * either yank a live offer out from under an offeree that is simply slow to
5521
+ * poll, or (set generously) leave a genuinely dead offeree's task stranded
5522
+ * for the full window regardless of how obviously offline it already is.
5523
+ * `agents.status = 'offline'` already encodes the staleness threshold that
5524
+ * matters — `runtimeStaleThresholdMinutes()` / `RUNTIME_STALE_THRESHOLD_MIN`
5525
+ * for multi-runtime expiry, or immediate for an explicit close — so no
5526
+ * separate timeout knob is introduced here.
5527
+ */
5528
+ export function releaseStaleOfferedTasksForOfflineAgents(): number {
5529
+ const now = new Date().toISOString();
5530
+
5531
+ const result = getDb().run(
5532
+ `UPDATE agent_tasks
5533
+ SET status = 'unassigned', offeredTo = NULL, offeredAt = NULL, lastUpdatedAt = ?
5534
+ WHERE status = 'offered'
5535
+ AND offeredTo IS NOT NULL
5536
+ AND NOT EXISTS (
5537
+ SELECT 1 FROM agents WHERE agents.id = agent_tasks.offeredTo AND agents.status != 'offline'
5538
+ )`,
5539
+ [now],
5540
+ );
5541
+
5542
+ return result.changes;
5543
+ }
5544
+
5333
5545
  export function getOfferedTasksForAgent(agentId: string): AgentTask[] {
5334
5546
  return getDb()
5335
5547
  .prepare<AgentTaskRow, [string]>(
@@ -5556,6 +5768,7 @@ export {
5556
5768
  export function updateAgentProfile(
5557
5769
  id: string,
5558
5770
  updates: {
5771
+ name?: string;
5559
5772
  description?: string;
5560
5773
  role?: string;
5561
5774
  capabilities?: string[];
@@ -5579,6 +5792,31 @@ export function updateAgentProfile(
5579
5792
  .get(id);
5580
5793
  if (!current) return null;
5581
5794
 
5795
+ for (const field of BUDGETED_IDENTITY_FIELDS) {
5796
+ const nextValue = updates[field];
5797
+ if (nextValue === undefined) continue;
5798
+
5799
+ const result = checkIdentityFieldBudget({
5800
+ field,
5801
+ currentValue: current[field] ?? "",
5802
+ nextValue,
5803
+ });
5804
+ if (!result.ok) {
5805
+ throw new IdentityFieldBudgetError(
5806
+ result,
5807
+ computeContentHash(current[field] ?? ""),
5808
+ computeContentHash(nextValue),
5809
+ );
5810
+ }
5811
+ }
5812
+
5813
+ if (updates.name !== undefined) {
5814
+ const existingAgent = database
5815
+ .prepare<AgentRow, [string, string]>("SELECT * FROM agents WHERE name = ? AND id != ?")
5816
+ .get(updates.name, id);
5817
+ if (existingAgent) throw new Error("Agent name already exists");
5818
+ }
5819
+
5582
5820
  // Create context versions for changed fields
5583
5821
  for (const field of VERSIONABLE_FIELDS) {
5584
5822
  const newValue = updates[field];
@@ -5631,6 +5869,7 @@ export function updateAgentProfile(
5631
5869
  string | null,
5632
5870
  string | null,
5633
5871
  string | null,
5872
+ string | null,
5634
5873
  number,
5635
5874
  string | null,
5636
5875
  string,
@@ -5638,6 +5877,7 @@ export function updateAgentProfile(
5638
5877
  ]
5639
5878
  >(
5640
5879
  `UPDATE agents SET
5880
+ name = COALESCE(?, name),
5641
5881
  description = COALESCE(?, description),
5642
5882
  role = COALESCE(?, role),
5643
5883
  capabilities = COALESCE(?, capabilities),
@@ -5652,6 +5892,7 @@ export function updateAgentProfile(
5652
5892
  WHERE id = ? RETURNING *`,
5653
5893
  )
5654
5894
  .get(
5895
+ updates.name ?? null,
5655
5896
  updates.description ?? null,
5656
5897
  updates.role ?? null,
5657
5898
  updates.capabilities ? JSON.stringify(updates.capabilities) : null,
@@ -5672,23 +5913,7 @@ export function updateAgentProfile(
5672
5913
  }
5673
5914
 
5674
5915
  export function updateAgentName(id: string, newName: string): Agent | null {
5675
- // Check if another agent already has this name
5676
- const existingAgent = getDb()
5677
- .prepare<AgentRow, [string, string]>("SELECT * FROM agents WHERE name = ? AND id != ?")
5678
- .get(newName, id);
5679
-
5680
- if (existingAgent) {
5681
- throw new Error("Agent name already exists");
5682
- }
5683
-
5684
- const now = new Date().toISOString();
5685
- const row = getDb()
5686
- .prepare<AgentRow, [string, string, string]>(
5687
- "UPDATE agents SET name = ?, lastUpdatedAt = ? WHERE id = ? RETURNING *",
5688
- )
5689
- .get(newName, now, id);
5690
-
5691
- return row ? rowToAgent(row) : null;
5916
+ return updateAgentProfile(id, { name: newName });
5692
5917
  }
5693
5918
 
5694
5919
  // ============================================================================
@@ -6873,8 +7098,30 @@ export interface SessionCostSummaryTotals {
6873
7098
  totalDurationMs: number;
6874
7099
  totalSessions: number;
6875
7100
  avgCostPerSession: number;
6876
- /** Share of `totalCostUsd` whose task carries a human requester. */
7101
+ /**
7102
+ * Cost of tasks with a human requester, over `attributableCostUsd` (not
7103
+ * `totalCostUsd`) — see `attributableCostUsd` for why. Also excludes cost on
7104
+ * a structurally-human-free task that happens to carry a stale/inherited
7105
+ * `requestedByUserId` (measured 2026-08-19: ~5% of the naive-attributed
7106
+ * population), so the two totals stay a consistent partition.
7107
+ */
6877
7108
  attributedCostUsd: number;
7109
+ /**
7110
+ * The corrected coverage denominator: `totalCostUsd` minus the cost of
7111
+ * structurally-human-free tasks (heartbeat/boot-triage, scheduled runs, and
7112
+ * `source='system'` follow-ups whose parent itself had no human requester —
7113
+ * i.e. follow-ups of the swarm's own self-maintenance, not of human work).
7114
+ * These tasks have no human requester *by construction*; stamping one on
7115
+ * them would be a lie, not coverage, so they don't belong in the
7116
+ * denominator at all. `attributedCostUsd / attributableCostUsd` is the
7117
+ * number to show as "coverage" — dividing by `totalCostUsd` instead
7118
+ * silently deflates it with a population that could never have scored.
7119
+ */
7120
+ attributableCostUsd: number;
7121
+ /** Cost of the structurally-human-free population excluded from `attributableCostUsd`. */
7122
+ excludedCostUsd: number;
7123
+ /** Distinct tasks behind `excludedCostUsd` — surfaced so the UI can name the exclusion count, not just wave at a percentage. */
7124
+ excludedTaskCount: number;
6878
7125
  }
6879
7126
 
6880
7127
  export interface SessionCostDailyRow {
@@ -6907,6 +7154,60 @@ export interface SessionCostByUserRow {
6907
7154
  /** `opts.userId` sentinel selecting spend with no human requester. */
6908
7155
  export const UNATTRIBUTED_USER_ID = "unattributed";
6909
7156
 
7157
+ // Structurally-human-free tasks and their descendants. An explicitly supplied
7158
+ // requester is a human handoff and stops propagation; a requester copied from
7159
+ // the parent does not.
7160
+ const HUMAN_FREE_TASKS_CTE = `human_free_tasks(id) AS (
7161
+ SELECT task.id
7162
+ FROM agent_tasks task
7163
+ LEFT JOIN agent_tasks parent ON parent.id = task.parentTaskId
7164
+ WHERE COALESCE(task.taskType, '') IN ('heartbeat', 'heartbeat-checklist', 'boot-triage')
7165
+ OR COALESCE(task.tags, '[]') LIKE '%"heartbeat"%'
7166
+ OR (COALESCE(task.source, '') = 'schedule' AND task.requestedByUserId IS NULL)
7167
+ OR (
7168
+ task.parentTaskId IS NULL
7169
+ AND COALESCE(task.source, '') = 'workflow'
7170
+ AND task.requestedByUserId IS NULL
7171
+ AND EXISTS (
7172
+ SELECT 1
7173
+ FROM workflow_runs run
7174
+ WHERE run.id = task.workflowRunId
7175
+ AND run.triggerType = 'schedule'
7176
+ AND run.created_by IS NULL
7177
+ )
7178
+ )
7179
+ OR (
7180
+ COALESCE(task.source, '') = 'system'
7181
+ AND parent.id IS NOT NULL
7182
+ AND parent.requestedByUserId IS NULL
7183
+ )
7184
+
7185
+ UNION
7186
+
7187
+ SELECT child.id
7188
+ FROM agent_tasks child
7189
+ JOIN human_free_tasks parent ON child.parentTaskId = parent.id
7190
+ WHERE child.requestedByUserId IS NULL
7191
+ OR child.requestedByUserIdInherited = 1
7192
+ )`;
7193
+ const HUMAN_FREE_SQL = "EXISTS (SELECT 1 FROM human_free_tasks WHERE id = t.id)";
7194
+ const ROOT_HUMAN_FREE_SQL = `(
7195
+ COALESCE(t.taskType, '') IN ('heartbeat', 'heartbeat-checklist', 'boot-triage')
7196
+ OR COALESCE(t.tags, '[]') LIKE '%"heartbeat"%'
7197
+ OR (COALESCE(t.source, '') = 'schedule' AND t.requestedByUserId IS NULL)
7198
+ OR (
7199
+ COALESCE(t.source, '') = 'workflow'
7200
+ AND t.requestedByUserId IS NULL
7201
+ AND EXISTS (
7202
+ SELECT 1
7203
+ FROM workflow_runs run
7204
+ WHERE run.id = t.workflowRunId
7205
+ AND run.triggerType = 'schedule'
7206
+ AND run.created_by IS NULL
7207
+ )
7208
+ )
7209
+ )`;
7210
+
6910
7211
  export function getSessionCostSummary(opts: {
6911
7212
  startDate?: string;
6912
7213
  endDate?: string;
@@ -6925,6 +7226,15 @@ export function getSessionCostSummary(opts: {
6925
7226
  // through the task (same shape as `getDailySpendForUser`). Every column is
6926
7227
  // `sc.`-qualified because `createdAt`/`agentId` exist on both sides.
6927
7228
  const from = "FROM session_costs sc LEFT JOIN agent_tasks t ON t.id = sc.taskId";
7229
+
7230
+ // Structurally-human-free: the swarm maintaining itself, with no human
7231
+ // requester by construction — heartbeat/boot-triage tasks, scheduled runs
7232
+ // without a human creator (including workflow roots launched by such a
7233
+ // schedule), and `source='system'` follow-ups whose parent itself has no
7234
+ // human requester. That classification propagates through descendants while
7235
+ // they remain unattributed, so autonomous fan-out cannot leak back into the
7236
+ // denominator. An explicitly attributed child is an independent handoff and
7237
+ // stops propagation down that branch.
6928
7238
  const conditions: string[] = [];
6929
7239
  const params: string[] = [];
6930
7240
 
@@ -6941,9 +7251,9 @@ export function getSessionCostSummary(opts: {
6941
7251
  params.push(opts.agentId);
6942
7252
  }
6943
7253
  if (opts.userId === UNATTRIBUTED_USER_ID) {
6944
- conditions.push("t.requestedByUserId IS NULL");
7254
+ conditions.push(`(t.requestedByUserId IS NULL OR ${HUMAN_FREE_SQL})`);
6945
7255
  } else if (opts.userId) {
6946
- conditions.push("t.requestedByUserId = ?");
7256
+ conditions.push(`(t.requestedByUserId = ? AND NOT ${HUMAN_FREE_SQL})`);
6947
7257
  params.push(opts.userId);
6948
7258
  }
6949
7259
 
@@ -6959,11 +7269,15 @@ export function getSessionCostSummary(opts: {
6959
7269
  totalDurationMs: number;
6960
7270
  totalSessions: number;
6961
7271
  attributedCostUsd: number;
7272
+ attributableCostUsd: number;
7273
+ excludedCostUsd: number;
7274
+ excludedTaskCount: number;
6962
7275
  };
6963
7276
 
6964
7277
  const totalsRow = getDb()
6965
7278
  .prepare<TotalsRow, string[]>(
6966
- `SELECT
7279
+ `WITH RECURSIVE ${HUMAN_FREE_TASKS_CTE}
7280
+ SELECT
6967
7281
  COALESCE(SUM(sc.totalCostUsd), 0) as totalCostUsd,
6968
7282
  COALESCE(SUM(sc.inputTokens), 0) as totalInputTokens,
6969
7283
  COALESCE(SUM(sc.outputTokens), 0) as totalOutputTokens,
@@ -6971,8 +7285,13 @@ export function getSessionCostSummary(opts: {
6971
7285
  COALESCE(SUM(sc.cacheWriteTokens), 0) as totalCacheWriteTokens,
6972
7286
  COALESCE(SUM(sc.durationMs), 0) as totalDurationMs,
6973
7287
  COUNT(*) as totalSessions,
6974
- COALESCE(SUM(CASE WHEN t.requestedByUserId IS NOT NULL THEN sc.totalCostUsd ELSE 0 END), 0)
6975
- as attributedCostUsd
7288
+ COALESCE(SUM(CASE WHEN t.requestedByUserId IS NOT NULL AND NOT ${HUMAN_FREE_SQL}
7289
+ THEN sc.totalCostUsd ELSE 0 END), 0) as attributedCostUsd,
7290
+ COALESCE(SUM(CASE WHEN NOT ${HUMAN_FREE_SQL}
7291
+ THEN sc.totalCostUsd ELSE 0 END), 0) as attributableCostUsd,
7292
+ COALESCE(SUM(CASE WHEN ${HUMAN_FREE_SQL}
7293
+ THEN sc.totalCostUsd ELSE 0 END), 0) as excludedCostUsd,
7294
+ COUNT(DISTINCT CASE WHEN ${HUMAN_FREE_SQL} THEN t.id END) as excludedTaskCount
6976
7295
  ${from} ${where}`,
6977
7296
  )
6978
7297
  .get(...params);
@@ -6993,6 +7312,9 @@ export function getSessionCostSummary(opts: {
6993
7312
  totalSessions: 0,
6994
7313
  avgCostPerSession: 0,
6995
7314
  attributedCostUsd: 0,
7315
+ attributableCostUsd: 0,
7316
+ excludedCostUsd: 0,
7317
+ excludedTaskCount: 0,
6996
7318
  };
6997
7319
 
6998
7320
  // Daily breakdown
@@ -7010,7 +7332,8 @@ export function getSessionCostSummary(opts: {
7010
7332
  },
7011
7333
  string[]
7012
7334
  >(
7013
- `SELECT
7335
+ `WITH RECURSIVE ${HUMAN_FREE_TASKS_CTE}
7336
+ SELECT
7014
7337
  DATE(sc.createdAt) as date,
7015
7338
  COALESCE(SUM(sc.totalCostUsd), 0) as costUsd,
7016
7339
  COALESCE(SUM(sc.inputTokens), 0) as inputTokens,
@@ -7038,7 +7361,8 @@ export function getSessionCostSummary(opts: {
7038
7361
  },
7039
7362
  string[]
7040
7363
  >(
7041
- `SELECT
7364
+ `WITH RECURSIVE ${HUMAN_FREE_TASKS_CTE}
7365
+ SELECT
7042
7366
  sc.agentId as agentId,
7043
7367
  COALESCE(SUM(sc.totalCostUsd), 0) as costUsd,
7044
7368
  COALESCE(SUM(sc.inputTokens), 0) as inputTokens,
@@ -7052,22 +7376,22 @@ export function getSessionCostSummary(opts: {
7052
7376
  .all(...params);
7053
7377
  }
7054
7378
 
7055
- // Per-requester breakdown. The `userId IS NULL` bucket is a real row, not a
7056
- // gap: autonomous spend (heartbeat, boot triage) has no human requester and
7057
- // must stay visible rather than being folded into a person.
7379
+ // Per-requester breakdown. Structurally-human-free work is assigned to the
7380
+ // autonomous bucket even if a stale requester id survives on the task.
7058
7381
  let byUser: SessionCostByUserRow[] = [];
7059
7382
  if (groupBy === "user" || groupBy === "both") {
7060
7383
  byUser = getDb()
7061
7384
  .prepare<SessionCostByUserRow, string[]>(
7062
- `SELECT
7063
- t.requestedByUserId as userId,
7385
+ `WITH RECURSIVE ${HUMAN_FREE_TASKS_CTE}
7386
+ SELECT
7387
+ CASE WHEN ${HUMAN_FREE_SQL} THEN NULL ELSE t.requestedByUserId END as userId,
7064
7388
  COALESCE(SUM(sc.totalCostUsd), 0) as costUsd,
7065
7389
  COALESCE(SUM(sc.inputTokens), 0) as inputTokens,
7066
7390
  COALESCE(SUM(sc.outputTokens), 0) as outputTokens,
7067
7391
  COUNT(DISTINCT sc.taskId) as tasks,
7068
7392
  COALESCE(SUM(sc.durationMs), 0) as durationMs
7069
7393
  ${from} ${where}
7070
- GROUP BY t.requestedByUserId
7394
+ GROUP BY CASE WHEN ${HUMAN_FREE_SQL} THEN NULL ELSE t.requestedByUserId END
7071
7395
  ORDER BY costUsd DESC`,
7072
7396
  )
7073
7397
  .all(...params);
@@ -7076,6 +7400,235 @@ export function getSessionCostSummary(opts: {
7076
7400
  return { totals, daily, byAgent, byUser };
7077
7401
  }
7078
7402
 
7403
+ // --- Per-person attribution (four-metric view) ---
7404
+
7405
+ export interface AttributionByPersonRow {
7406
+ userId: string;
7407
+ /** Root tasks (`parentTaskId IS NULL`) only — counting fan-out children would let
7408
+ * whoever triggers the biggest decomposition win by accident. */
7409
+ problemsInitiated: number;
7410
+ /** Roots that completed AND show shippable evidence — see `getAttributionByPerson` doc. */
7411
+ problemsShipped: number;
7412
+ /** Distinct agents engaged across the person's entire task tree (not root-scoped —
7413
+ * a root that fans out to nine agents is nine agents of reach). */
7414
+ agentsReached: number;
7415
+ reposReached: number;
7416
+ surfacesReached: number;
7417
+ /**
7418
+ * Share of root tasks completed without a re-dispatch or a human correction.
7419
+ * Deliberately `null`: distinguishing a legitimate multi-agent fan-out from a
7420
+ * correction-driven re-dispatch isn't computable from a single query pass
7421
+ * (would need to inspect *why* a child task was created, which isn't a stored
7422
+ * fact), and a task-count-based proxy would misclassify normal delegation as
7423
+ * rework. Render as "not yet computed" — do not invent a stand-in.
7424
+ */
7425
+ firstPassYield: null;
7426
+ }
7427
+
7428
+ /**
7429
+ * Four metrics per human requester, reported side by side — never summed into
7430
+ * a composite score. Scope is root tasks (`problemsInitiated`/`problemsShipped`)
7431
+ * or the person's entire task tree (`*Reached`); `requestedByUserId IS NULL`
7432
+ * (autonomous work) and structurally-human-free rows with a stale/inherited
7433
+ * requester (see `HUMAN_FREE_SQL` in `getSessionCostSummary`) are excluded —
7434
+ * neither belongs to a person.
7435
+ *
7436
+ * "Problems shipped" detection walks each root's full task tree, preferring a
7437
+ * `task_attachments` row (`kind='url'` matching a GitHub PR or GitLab MR URL,
7438
+ * or `kind='page'` for a published artifact) and falling back to the same
7439
+ * provider-specific URL match on any task output. GitLab matching is host
7440
+ * agnostic so self-hosted instances are covered. This does NOT detect a closed ticket (no
7441
+ * Linear/Jira issue-state table exists locally) — "shipped" undercounts
7442
+ * ticket-only outcomes until the real artifacts join lands.
7443
+ */
7444
+ export function getAttributionByPerson(opts: {
7445
+ startDate?: string;
7446
+ endDate?: string;
7447
+ }): AttributionByPersonRow[] {
7448
+ const conditions: string[] = ["t.requestedByUserId IS NOT NULL"];
7449
+ const params: string[] = [];
7450
+ if (opts.startDate) {
7451
+ conditions.push("t.createdAt >= ?");
7452
+ params.push(opts.startDate);
7453
+ }
7454
+ if (opts.endDate) {
7455
+ // A date-only value represents the whole UTC day. Preserve inclusive
7456
+ // timestamp semantics for callers that provide an exact instant.
7457
+ const dateOnlyEnd = /^\d{4}-\d{2}-\d{2}$/.test(opts.endDate)
7458
+ ? new Date(`${opts.endDate}T00:00:00.000Z`)
7459
+ : null;
7460
+ if (
7461
+ dateOnlyEnd &&
7462
+ !Number.isNaN(dateOnlyEnd.getTime()) &&
7463
+ dateOnlyEnd.toISOString().slice(0, 10) === opts.endDate
7464
+ ) {
7465
+ dateOnlyEnd.setUTCDate(dateOnlyEnd.getUTCDate() + 1);
7466
+ conditions.push("t.createdAt < ?");
7467
+ params.push(dateOnlyEnd.toISOString());
7468
+ } else {
7469
+ conditions.push("t.createdAt <= ?");
7470
+ params.push(opts.endDate);
7471
+ }
7472
+ }
7473
+ const where = `WHERE ${conditions.join(" AND ")}`;
7474
+
7475
+ type RootRow = { userId: string; initiated: number; shipped: number };
7476
+ const rootRows = getDb()
7477
+ .prepare<RootRow, string[]>(
7478
+ `WITH RECURSIVE selected_roots(id, requestedByUserId, status, output) AS (
7479
+ SELECT t.id, t.requestedByUserId, t.status, t.output
7480
+ FROM agent_tasks t
7481
+ ${where} AND t.parentTaskId IS NULL AND NOT ${ROOT_HUMAN_FREE_SQL}
7482
+ ),
7483
+ task_tree(rootId, taskId, output) AS (
7484
+ SELECT id, id, output
7485
+ FROM selected_roots
7486
+
7487
+ UNION ALL
7488
+
7489
+ SELECT tree.rootId, child.id, child.output
7490
+ FROM agent_tasks child
7491
+ JOIN task_tree tree ON child.parentTaskId = tree.taskId
7492
+ )
7493
+ SELECT
7494
+ t.requestedByUserId as userId,
7495
+ COUNT(*) as initiated,
7496
+ SUM(CASE WHEN t.status = 'completed' AND (
7497
+ EXISTS (
7498
+ SELECT 1
7499
+ FROM task_tree tree
7500
+ JOIN task_attachments ta ON ta.task_id = tree.taskId
7501
+ WHERE tree.rootId = t.id
7502
+ AND ta.kind = 'url'
7503
+ AND (
7504
+ ta.url LIKE '%github.com/%/pull/%'
7505
+ OR ta.url LIKE '%/-/merge_requests/%'
7506
+ )
7507
+ )
7508
+ OR EXISTS (
7509
+ SELECT 1 FROM task_attachments ta
7510
+ JOIN task_tree tree ON tree.taskId = ta.task_id
7511
+ WHERE tree.rootId = t.id AND ta.kind = 'page'
7512
+ )
7513
+ OR EXISTS (
7514
+ SELECT 1 FROM task_tree tree
7515
+ WHERE tree.rootId = t.id
7516
+ AND (
7517
+ tree.output LIKE '%github.com/%/pull/%'
7518
+ OR tree.output LIKE '%/-/merge_requests/%'
7519
+ )
7520
+ )
7521
+ ) THEN 1 ELSE 0 END) as shipped
7522
+ FROM selected_roots t
7523
+ GROUP BY t.requestedByUserId`,
7524
+ )
7525
+ .all(...params);
7526
+
7527
+ type ReachRow = {
7528
+ userId: string;
7529
+ agentsReached: number;
7530
+ reposReached: number;
7531
+ surfacesReached: number;
7532
+ };
7533
+ const reachRows = getDb()
7534
+ .prepare<ReachRow, string[]>(
7535
+ `WITH RECURSIVE report_tasks AS (
7536
+ SELECT t.*
7537
+ FROM agent_tasks t
7538
+ ${where}
7539
+ ),
7540
+ task_ancestry(
7541
+ taskId, id, parentTaskId, requestedByUserId, requestedByUserIdInherited,
7542
+ taskType, tags, source, workflowRunId
7543
+ ) AS (
7544
+ SELECT
7545
+ id, id, parentTaskId, requestedByUserId, requestedByUserIdInherited,
7546
+ taskType, tags, source, workflowRunId
7547
+ FROM report_tasks
7548
+
7549
+ UNION ALL
7550
+
7551
+ SELECT
7552
+ child.taskId, parent.id, parent.parentTaskId, parent.requestedByUserId,
7553
+ parent.requestedByUserIdInherited, parent.taskType, parent.tags,
7554
+ parent.source, parent.workflowRunId
7555
+ FROM agent_tasks parent
7556
+ JOIN task_ancestry child ON parent.id = child.parentTaskId
7557
+ WHERE child.requestedByUserId IS NULL
7558
+ OR child.requestedByUserIdInherited = 1
7559
+ ),
7560
+ human_free_report_tasks(id) AS (
7561
+ SELECT DISTINCT ancestor.taskId
7562
+ FROM task_ancestry ancestor
7563
+ LEFT JOIN agent_tasks parent ON parent.id = ancestor.parentTaskId
7564
+ WHERE COALESCE(ancestor.taskType, '') IN ('heartbeat', 'heartbeat-checklist', 'boot-triage')
7565
+ OR COALESCE(ancestor.tags, '[]') LIKE '%"heartbeat"%'
7566
+ OR (COALESCE(ancestor.source, '') = 'schedule' AND ancestor.requestedByUserId IS NULL)
7567
+ OR (
7568
+ ancestor.parentTaskId IS NULL
7569
+ AND COALESCE(ancestor.source, '') = 'workflow'
7570
+ AND ancestor.requestedByUserId IS NULL
7571
+ AND EXISTS (
7572
+ SELECT 1
7573
+ FROM workflow_runs run
7574
+ WHERE run.id = ancestor.workflowRunId
7575
+ AND run.triggerType = 'schedule'
7576
+ AND run.created_by IS NULL
7577
+ )
7578
+ )
7579
+ OR (
7580
+ COALESCE(ancestor.source, '') = 'system'
7581
+ AND parent.id IS NOT NULL
7582
+ AND parent.requestedByUserId IS NULL
7583
+ )
7584
+ )
7585
+ SELECT
7586
+ t.requestedByUserId as userId,
7587
+ COUNT(DISTINCT t.agentId) as agentsReached,
7588
+ COUNT(DISTINCT t.vcsRepo) as reposReached,
7589
+ COUNT(DISTINCT t.source) as surfacesReached
7590
+ FROM report_tasks t
7591
+ WHERE NOT EXISTS (SELECT 1 FROM human_free_report_tasks WHERE id = t.id)
7592
+ GROUP BY t.requestedByUserId`,
7593
+ )
7594
+ .all(...params);
7595
+
7596
+ const byUser = new Map<string, AttributionByPersonRow>();
7597
+ for (const r of rootRows) {
7598
+ byUser.set(r.userId, {
7599
+ userId: r.userId,
7600
+ problemsInitiated: r.initiated,
7601
+ problemsShipped: r.shipped,
7602
+ agentsReached: 0,
7603
+ reposReached: 0,
7604
+ surfacesReached: 0,
7605
+ firstPassYield: null,
7606
+ });
7607
+ }
7608
+ for (const r of reachRows) {
7609
+ const existing = byUser.get(r.userId);
7610
+ if (existing) {
7611
+ existing.agentsReached = r.agentsReached;
7612
+ existing.reposReached = r.reposReached;
7613
+ existing.surfacesReached = r.surfacesReached;
7614
+ } else {
7615
+ // Reach with no root task in-window (e.g. only fan-out children whose
7616
+ // root predates the window) — still a real person row.
7617
+ byUser.set(r.userId, {
7618
+ userId: r.userId,
7619
+ problemsInitiated: 0,
7620
+ problemsShipped: 0,
7621
+ agentsReached: r.agentsReached,
7622
+ reposReached: r.reposReached,
7623
+ surfacesReached: r.surfacesReached,
7624
+ firstPassYield: null,
7625
+ });
7626
+ }
7627
+ }
7628
+
7629
+ return Array.from(byUser.values());
7630
+ }
7631
+
7079
7632
  // --- Dashboard cost summary (P4) ---
7080
7633
 
7081
7634
  export interface DashboardCostSummary {
@@ -8224,6 +8777,10 @@ export function upsertSwarmConfig(data: {
8224
8777
  }
8225
8778
  }
8226
8779
 
8780
+ if (config.isSecret) {
8781
+ registerVolatileSecret(config.value, `config:${config.key}`);
8782
+ }
8783
+
8227
8784
  return config;
8228
8785
  }
8229
8786
 
@@ -8565,6 +9122,7 @@ export function insertActiveSession(session: {
8565
9122
  inboxMessageId?: string;
8566
9123
  taskDescription?: string;
8567
9124
  runnerSessionId?: string;
9125
+ runtimeInstanceId?: string;
8568
9126
  }): ActiveSession {
8569
9127
  const id = crypto.randomUUID();
8570
9128
  const now = new Date().toISOString();
@@ -8580,12 +9138,13 @@ export function insertActiveSession(session: {
8580
9138
  string | null,
8581
9139
  string | null,
8582
9140
  string | null,
9141
+ string | null,
8583
9142
  string,
8584
9143
  string,
8585
9144
  ]
8586
9145
  >(
8587
- `INSERT INTO active_sessions (id, agentId, taskId, triggerType, inboxMessageId, taskDescription, runnerSessionId, startedAt, lastHeartbeatAt)
8588
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
9146
+ `INSERT INTO active_sessions (id, agentId, taskId, triggerType, inboxMessageId, taskDescription, runnerSessionId, runtimeInstanceId, startedAt, lastHeartbeatAt)
9147
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
8589
9148
  RETURNING *`,
8590
9149
  )
8591
9150
  .get(
@@ -8596,6 +9155,7 @@ export function insertActiveSession(session: {
8596
9155
  session.inboxMessageId ?? null,
8597
9156
  session.taskDescription ?? null,
8598
9157
  session.runnerSessionId ?? null,
9158
+ session.runtimeInstanceId ?? null,
8599
9159
  now,
8600
9160
  now,
8601
9161
  );
@@ -9208,9 +9768,11 @@ type WorkflowRunRow = {
9208
9768
  id: string;
9209
9769
  workflowId: string;
9210
9770
  status: string;
9771
+ triggerType: string;
9211
9772
  triggerData: string | null;
9212
9773
  context: string | null;
9213
9774
  error: string | null;
9775
+ created_by: string | null;
9214
9776
  startedAt: string;
9215
9777
  lastUpdatedAt: string;
9216
9778
  finishedAt: string | null;
@@ -9224,6 +9786,7 @@ function rowToWorkflowRun(row: WorkflowRunRow): WorkflowRun {
9224
9786
  triggerData: row.triggerData ? JSON.parse(row.triggerData) : undefined,
9225
9787
  context: row.context ? (JSON.parse(row.context) as Record<string, unknown>) : undefined,
9226
9788
  error: row.error ?? undefined,
9789
+ createdBy: row.created_by ?? undefined,
9227
9790
  startedAt: normalizeDateRequired(row.startedAt),
9228
9791
  lastUpdatedAt: normalizeDateRequired(row.lastUpdatedAt),
9229
9792
  finishedAt: normalizeDate(row.finishedAt) ?? undefined,
@@ -9233,14 +9796,23 @@ function rowToWorkflowRun(row: WorkflowRunRow): WorkflowRun {
9233
9796
  export function createWorkflowRun(data: {
9234
9797
  id: string;
9235
9798
  workflowId: string;
9799
+ triggerType?: "schedule" | "manual" | "event" | "api";
9236
9800
  triggerData?: unknown;
9801
+ createdBy?: string;
9237
9802
  }): WorkflowRun {
9238
9803
  const now = new Date().toISOString();
9239
9804
  const row = getDb()
9240
- .prepare<WorkflowRunRow, [string, string, string, string | null]>(
9241
- `INSERT INTO workflow_runs (id, workflowId, startedAt, triggerData) VALUES (?, ?, ?, ?) RETURNING *`,
9805
+ .prepare<WorkflowRunRow, [string, string, string, string, string | null, string | null]>(
9806
+ `INSERT INTO workflow_runs (id, workflowId, triggerType, startedAt, triggerData, created_by) VALUES (?, ?, ?, ?, ?, ?) RETURNING *`,
9242
9807
  )
9243
- .get(data.id, data.workflowId, now, data.triggerData ? JSON.stringify(data.triggerData) : null);
9808
+ .get(
9809
+ data.id,
9810
+ data.workflowId,
9811
+ data.triggerType ?? "manual",
9812
+ now,
9813
+ data.triggerData ? JSON.stringify(data.triggerData) : null,
9814
+ data.createdBy ?? null,
9815
+ );
9244
9816
  if (!row) throw new Error("Failed to create workflow run");
9245
9817
  return rowToWorkflowRun(row);
9246
9818
  }
@@ -13161,13 +13733,104 @@ export function updateUser(
13161
13733
  return row ? rowToUser(row) : null;
13162
13734
  }
13163
13735
 
13164
- export function deleteUser(id: string): boolean {
13165
- // Clear any task references before deleting
13166
- getDb()
13167
- .prepare("UPDATE agent_tasks SET requestedByUserId = NULL WHERE requestedByUserId = ?")
13168
- .run(id);
13169
- const result = getDb().prepare("DELETE FROM users WHERE id = ?").run(id);
13170
- return result.changes > 0;
13736
+ type UserReferenceRow = {
13737
+ tableName: string;
13738
+ columnName: string;
13739
+ };
13740
+
13741
+ function quoteSqlIdentifier(identifier: string): string {
13742
+ return `"${identifier.replaceAll('"', '""')}"`;
13743
+ }
13744
+
13745
+ export function deleteUser(id: string, replacementUserId?: string): boolean {
13746
+ if (replacementUserId === id) {
13747
+ throw new Error("Replacement user must differ from deleted user");
13748
+ }
13749
+
13750
+ const database = getDb();
13751
+ return database.transaction(() => {
13752
+ const userExists = database
13753
+ .prepare<{ present: number }, [string]>(
13754
+ "SELECT EXISTS(SELECT 1 FROM users WHERE id = ?) AS present",
13755
+ )
13756
+ .get(id)?.present;
13757
+ if (!userExists) return false;
13758
+
13759
+ if (replacementUserId) {
13760
+ const replacementExists = database
13761
+ .prepare<{ present: number }, [string]>(
13762
+ "SELECT EXISTS(SELECT 1 FROM users WHERE id = ?) AS present",
13763
+ )
13764
+ .get(replacementUserId)?.present;
13765
+ if (!replacementExists) throw new Error("Replacement user not found");
13766
+ }
13767
+
13768
+ // Preserve rows that carry user attribution but lack ON DELETE semantics.
13769
+ // Schema discovery keeps this correct as new nullable user audit columns are added.
13770
+ // Migration 103 accidentally dropped the scheduled_tasks audit FKs while
13771
+ // recreating that table, so discover those two known logical references as
13772
+ // well until the table is next rebuilt with its original constraints.
13773
+ const references = database
13774
+ .prepare<UserReferenceRow, []>(
13775
+ `SELECT tables.name AS tableName, foreign_keys."from" AS columnName
13776
+ FROM sqlite_schema AS tables
13777
+ JOIN pragma_foreign_key_list(tables.name) AS foreign_keys
13778
+ JOIN pragma_table_info(tables.name) AS columns
13779
+ ON columns.name = foreign_keys."from"
13780
+ WHERE tables.type = 'table'
13781
+ AND foreign_keys."table" = 'users'
13782
+ AND foreign_keys.on_delete IN ('NO ACTION', 'RESTRICT')
13783
+ AND columns."notnull" = 0
13784
+ UNION ALL
13785
+ SELECT 'scheduled_tasks' AS tableName, columns.name AS columnName
13786
+ FROM pragma_table_info('scheduled_tasks') AS columns
13787
+ WHERE columns.name IN ('created_by', 'updated_by')
13788
+ AND columns."notnull" = 0
13789
+ AND NOT EXISTS (
13790
+ SELECT 1
13791
+ FROM pragma_foreign_key_list('scheduled_tasks') AS foreign_keys
13792
+ WHERE foreign_keys."from" = columns.name
13793
+ AND foreign_keys."table" = 'users'
13794
+ )`,
13795
+ )
13796
+ .all();
13797
+ const replacement = replacementUserId ?? null;
13798
+ for (const reference of references) {
13799
+ const table = quoteSqlIdentifier(reference.tableName);
13800
+ const column = quoteSqlIdentifier(reference.columnName);
13801
+ database
13802
+ .prepare<unknown, [string | null, string]>(
13803
+ `UPDATE ${table} SET ${column} = ? WHERE ${column} = ?`,
13804
+ )
13805
+ .run(replacement, id);
13806
+ }
13807
+
13808
+ // Workflow context is persisted JSON rather than a relational column, but
13809
+ // it exposes the same requester identity to downstream interpolation. Keep
13810
+ // it consistent with workflow_runs.created_by inside this transaction.
13811
+ if (replacementUserId) {
13812
+ database
13813
+ .prepare<unknown, [string, string]>(
13814
+ `UPDATE workflow_runs
13815
+ SET context = json_set(context, '$.swarm.requestedByUserId', ?)
13816
+ WHERE json_valid(context)
13817
+ AND json_extract(context, '$.swarm.requestedByUserId') = ?`,
13818
+ )
13819
+ .run(replacementUserId, id);
13820
+ } else {
13821
+ database
13822
+ .prepare<unknown, [string]>(
13823
+ `UPDATE workflow_runs
13824
+ SET context = json_remove(context, '$.swarm.requestedByUserId')
13825
+ WHERE json_valid(context)
13826
+ AND json_extract(context, '$.swarm.requestedByUserId') = ?`,
13827
+ )
13828
+ .run(id);
13829
+ }
13830
+
13831
+ const result = database.prepare("DELETE FROM users WHERE id = ?").run(id);
13832
+ return result.changes > 0;
13833
+ })();
13171
13834
  }
13172
13835
 
13173
13836
  // ============================================================================