@desplega.ai/agent-swarm 1.158.0 → 1.159.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 (272) hide show
  1. package/dist/{acp-adapter-xaq4n6pc.js → acp-adapter-qhch0n2v.js} +1 -1
  2. package/dist/{actions-p9hz5yat.js → actions-dtsh1mfb.js} +33 -29
  3. package/dist/{anthropic-messages-51pz26qe.js → anthropic-messages-f2h06k64.js} +8 -3
  4. package/dist/{app-g26wdcz6.js → app-y1b9srva.js} +10 -9
  5. package/dist/{assistant-wydfqkb3.js → assistant-31hwy5xj.js} +34 -30
  6. package/dist/{azure-openai-responses-8gksqtmz.js → azure-openai-responses-0k1mfax5.js} +12 -13
  7. package/dist/{boot-reembed-se8zmdcp.js → boot-reembed-48d2qpkt.js} +11 -11
  8. package/dist/{boot-reembed-g5ckg9yb.js → boot-reembed-w0bryhkr.js} +12 -12
  9. package/dist/{boot-scrub-logs-wfxqan61.js → boot-scrub-logs-t65wbrn7.js} +9 -8
  10. package/dist/{boot-seeds-s3q01xj3.js → boot-seeds-h7c32hvm.js} +12 -11
  11. package/dist/{claude-adapter-tv2x4f36.js → claude-adapter-1t6611ne.js} +26 -21
  12. package/dist/{claude-managed-adapter-41b2s7fd.js → claude-managed-adapter-werp659j.js} +4 -2
  13. package/dist/{claude-managed-setup-60g8aazx.js → claude-managed-setup-y6rdgbc1.js} +11 -2
  14. package/dist/claude-sdk-session-zhkyk8yw.js +29319 -0
  15. package/dist/{cli-vj6h9kc2.js → cli-0m7wct0d.js} +59 -1
  16. package/dist/{cli-wscdvcqz.js → cli-0n2e4x49.js} +1 -1
  17. package/dist/cli-0xqde53b.js +176 -0
  18. package/dist/{cli-xberxa1r.js → cli-10ym38qc.js} +2 -2
  19. package/dist/{cli-5b6b62tz.js → cli-13hw4z91.js} +1 -1
  20. package/dist/{cli-rqz8qbw7.js → cli-20035trh.js} +50 -18
  21. package/dist/{cli-k0g1sjzm.js → cli-31nkw3g3.js} +3 -3
  22. package/dist/cli-3gvrr8vj.js +1422 -0
  23. package/dist/{cli-c07r9a2m.js → cli-48527fpw.js} +9 -8
  24. package/dist/{cli-24k806sc.js → cli-4aj25szq.js} +2 -2
  25. package/dist/{cli-7gtvvpbh.js → cli-4tvgz99m.js} +7 -7
  26. package/dist/cli-503ya656.js +455 -0
  27. package/dist/{cli-cw9zck8j.js → cli-5f2hq128.js} +8 -4
  28. package/dist/{cli-s8eet254.js → cli-68bmme5p.js} +11078 -27833
  29. package/dist/{cli-ftfvggzx.js → cli-79aq9cgb.js} +794 -563
  30. package/dist/cli-7cxw8b60.js +14910 -0
  31. package/dist/cli-7kyk9jr3.js +22 -0
  32. package/dist/cli-7zttzetj.js +324 -0
  33. package/dist/{cli-y95wp3tg.js → cli-96qd5psa.js} +1 -1
  34. package/dist/{cli-g8djp6qe.js → cli-9yg65c1j.js} +22 -0
  35. package/dist/{cli-3gmcwfyx.js → cli-ardpzhjm.js} +5 -5
  36. package/dist/{cli-7s3syk22.js → cli-b6thtzac.js} +5 -5
  37. package/dist/{cli-00w2c66p.js → cli-bgv70kym.js} +13 -2
  38. package/dist/{cli-j55n64b6.js → cli-bv0a38v9.js} +163 -44
  39. package/dist/cli-ca2w7zv7.js +115 -0
  40. package/dist/cli-cpvsaqsw.js +333 -0
  41. package/dist/cli-cygyqj3b.js +754 -0
  42. package/dist/{cli-x24pshtq.js → cli-dvjpkkjd.js} +6 -6
  43. package/dist/{cli-5d10vd2m.js → cli-e0ndxhtk.js} +4 -4
  44. package/dist/{cli-4198qc9v.js → cli-e4xnc0qp.js} +15 -3
  45. package/dist/{cli-rjehvrts.js → cli-eg7cdg68.js} +8 -8
  46. package/dist/cli-ejpygnqz.js +3815 -0
  47. package/dist/{cli-fgqxwrpv.js → cli-fjtp3463.js} +1631 -127
  48. package/dist/{cli-zt1227k1.js → cli-fkqhbg8g.js} +5 -3
  49. package/dist/{cli-47m6dkd3.js → cli-g9df2m37.js} +409 -168
  50. package/dist/cli-gzyh0j0g.js +8455 -0
  51. package/dist/cli-hgqfttfc.js +183 -0
  52. package/dist/cli-hj65wsts.js +17755 -0
  53. package/dist/cli-j9z8mfx3.js +476 -0
  54. package/dist/{cli-aehm1rbq.js → cli-kc3dp84d.js} +1135 -61
  55. package/dist/{cli-h38x0qhg.js → cli-kdnehe0t.js} +4 -3
  56. package/dist/{cli-20cj37vt.js → cli-m4excttz.js} +136 -2216
  57. package/dist/{cli-8qmme6vd.js → cli-mg24hkt4.js} +2 -2
  58. package/dist/{cli-xhaeg2nc.js → cli-mzkqpxee.js} +1 -1
  59. package/dist/{cli-q67y4wzm.js → cli-phsaj4xq.js} +24 -2
  60. package/dist/{cli-pw9rj5t9.js → cli-pqetb83f.js} +1 -176
  61. package/dist/{cli-rj19hc34.js → cli-qjxy52w7.js} +1 -1
  62. package/dist/{cli-53sz8gkz.js → cli-qv4jv8bx.js} +1 -1
  63. package/dist/cli-qvnnx4ct.js +1051 -0
  64. package/dist/cli-rb9m6rpe.js +4980 -0
  65. package/dist/{cli-tt9k8r35.js → cli-rfezr056.js} +476 -909
  66. package/dist/{cli-ggm92wcn.js → cli-rgjprct3.js} +1 -0
  67. package/dist/{cli-9httdpyq.js → cli-rrqmqr1s.js} +1 -1
  68. package/dist/cli-rykfmgwg.js +102 -0
  69. package/dist/{cli-eetrvv91.js → cli-t3p77qk1.js} +1 -1
  70. package/dist/{cli-68w09sjw.js → cli-vn0qv9cj.js} +25 -88
  71. package/dist/{cli-987hg0aa.js → cli-vwbbaecp.js} +2 -2
  72. package/dist/cli-wd9hz40d.js +69 -0
  73. package/dist/{cli-j57qkrpq.js → cli-wgjx0q33.js} +13 -13
  74. package/dist/cli-xgpf00sk.js +2149 -0
  75. package/dist/{cli-mcnhgnn1.js → cli-ykk5jaax.js} +184 -26
  76. package/dist/{cli-aky8tctm.js → cli-z9x0sh1t.js} +3 -3
  77. package/dist/{cli-1hkrch3k.js → cli-zw65mab6.js} +5 -5
  78. package/dist/cli.js +38 -29
  79. package/dist/cloudflare-workers-ai-system-one-1sg586qv.js +46 -0
  80. package/dist/{codex-adapter-cnm8wk11.js → codex-adapter-jqfqqa21.js} +21 -14
  81. package/dist/{codex-hook-9hptmdhj.js → codex-hook-5eys3dya.js} +2 -2
  82. package/dist/{codex-session-runner-gfg547zj.js → codex-session-runner-zd341dsm.js} +21 -14
  83. package/dist/{commands-e3bn0p75.js → commands-vfsysz4s.js} +9 -8
  84. package/dist/{db-03v9kzr2.js → db-s3znhvbn.js} +15 -10
  85. package/dist/{dispatcher-nrmg8yht.js → dispatcher-5xn0c7rf.js} +32 -28
  86. package/dist/{dsh-adapter-k03nfqa6.js → dsh-adapter-894mgfvs.js} +3 -3
  87. package/dist/{e2b-93x1k0j5.js → e2b-ez07znam.js} +4 -3
  88. package/dist/execute-71ysgv35.js +965 -0
  89. package/dist/{google-generative-ai-bxmv50nc.js → google-generative-ai-1nfcdphp.js} +5 -4
  90. package/dist/{google-vertex-809af514.js → google-vertex-1tqcd7b1.js} +5 -4
  91. package/dist/{handlers-aqcedbyb.js → handlers-5w4jwycr.js} +35 -31
  92. package/dist/{hook-7vkskkre.js → hook-853bxrr6.js} +16 -13
  93. package/dist/{hook-ybvmrmhq.js → hook-z0hbxj85.js} +16 -13
  94. package/dist/{http-2eqa8t24.js → http-akc3wy3m.js} +600 -123
  95. package/dist/{index-f6cryxwe.js → index-4a5njnm1.js} +2 -59
  96. package/dist/{index-a3vn96n8.js → index-5sz16ksh.js} +187 -47
  97. package/dist/{index-c6zsv1bd.js → index-bjmjh9h4.js} +11 -11
  98. package/dist/{index-chgdkq36.js → index-cd151qkq.js} +32 -28
  99. package/dist/{index-x71bvq0p.js → index-k661dzzr.js} +33 -29
  100. package/dist/{index-1yks3qcr.js → index-ttpbhnjg.js} +32 -28
  101. package/dist/{keepalive-vtkf5n99.js → keepalive-8p9rpvz5.js} +11 -10
  102. package/dist/{lead-h3sp357x.js → lead-jp69z5h3.js} +49 -45
  103. package/dist/llama-cpp-classify-t2tccams.js +335 -0
  104. package/dist/{maintenance-jzeqtk3q.js → maintenance-fwx4ev0k.js} +12 -12
  105. package/dist/{mistral-conversations-zx0kn554.js → mistral-conversations-bhvwqcnk.js} +14 -17
  106. package/dist/{oauth-refresh-sweep-qp3dzmdp.js → oauth-refresh-sweep-cwcq1ctx.js} +10 -9
  107. package/dist/{onboard-tmsp0qbz.js → onboard-2b4q0t84.js} +6 -5
  108. package/dist/{openai-codex-responses-4vjqzvjp.js → openai-codex-responses-nekthgks.js} +21 -8
  109. package/dist/{openai-completions-9hjrqbw8.js → openai-completions-hxtzwj0s.js} +8 -11
  110. package/dist/{openai-responses-7b1pn0xt.js → openai-responses-w4r118w7.js} +22 -17
  111. package/dist/{opencode-adapter-x6x2da1z.js → opencode-adapter-zz7xvjj6.js} +6 -4
  112. package/dist/{openrouter-images-9f9ek4rw.js → openrouter-images-b6sqqjgn.js} +4 -6
  113. package/dist/{otel-impl-9mz73q65.js → otel-impl-g8rs4vwr.js} +1 -1
  114. package/dist/{pi-messages-txpjj39q.js → pi-messages-vxdt8v7j.js} +2 -1
  115. package/dist/{pi-mono-adapter-m6dvv0dz.js → pi-mono-adapter-n0me28wt.js} +29 -17
  116. package/dist/pricing-refresh-0v77385v.js +39 -0
  117. package/dist/{rbac-roles-c9wqz83x.js → rbac-roles-krjyazbj.js} +10 -9
  118. package/dist/{render-v2-gryrh9h8.js → render-v2-3wjsp9aw.js} +10 -9
  119. package/dist/runtime-ktjysacx.js +24 -0
  120. package/dist/{setup-q9720bt6.js → setup-yeqryke6.js} +6 -5
  121. package/dist/typesafe-system-one-96960jxp.js +27 -0
  122. package/dist/{virtual-modules-3htpgn8x.js → virtual-modules-dzdeq1sf.js} +21 -13
  123. package/dist/{work-objects-b76x7deb.js → work-objects-3pakpkw0.js} +35 -31
  124. package/dist/{worker-6vrxbv41.js → worker-85testgj.js} +49 -45
  125. package/openapi.json +863 -14
  126. package/package.json +9 -8
  127. package/src/be/db/agents.ts +20 -0
  128. package/src/be/db/tasks/human-free.ts +442 -0
  129. package/src/be/db/tasks/read.ts +56 -1
  130. package/src/be/db/tasks/write.ts +107 -11
  131. package/src/be/db.ts +89 -165
  132. package/src/be/harness-model-support.ts +182 -0
  133. package/src/be/human-free-drain.ts +67 -0
  134. package/src/be/migrations/182_task_human_free_flag.sql +63 -0
  135. package/src/be/migrations/183_human_free_reclassify_queue.sql +25 -0
  136. package/src/be/migrations/184_model_catalog.sql +120 -0
  137. package/src/be/model-catalog-store.ts +644 -0
  138. package/src/be/model-tier-keys.ts +31 -0
  139. package/src/be/model-tier-resolution.ts +489 -0
  140. package/src/be/model-validation.ts +112 -0
  141. package/src/be/models-catalog.ts +61 -7
  142. package/src/be/modelsdev-cache.json +82 -0
  143. package/src/be/modelsdev-cache.ts +2 -0
  144. package/src/be/pricing-refresh.ts +154 -12
  145. package/src/be/scripts/typecheck.ts +15 -0
  146. package/src/be/seed-scripts/catalog/task-failure-audit.ts +61 -9
  147. package/src/be/swarm-config-guard.ts +6 -0
  148. package/src/cli.tsx +10 -0
  149. package/src/commands/claude-managed-setup.ts +3 -1
  150. package/src/commands/runner.ts +76 -2
  151. package/src/extensions/contract-types.generated.ts +1 -1
  152. package/src/heartbeat/heartbeat.ts +64 -15
  153. package/src/heartbeat/templates.ts +2 -2
  154. package/src/http/agents.ts +49 -1
  155. package/src/http/index.ts +8 -0
  156. package/src/http/models-catalog.ts +357 -8
  157. package/src/http/poll.ts +45 -2
  158. package/src/http/schedules.ts +36 -6
  159. package/src/http/session-data.ts +2 -5
  160. package/src/http/stats.ts +6 -8
  161. package/src/http/tasks.ts +26 -1
  162. package/src/http/usage-cache.ts +56 -15
  163. package/src/http/workflows.ts +35 -8
  164. package/src/prompts/session-templates.ts +1 -0
  165. package/src/providers/claude-managed-models.ts +47 -161
  166. package/src/providers/codex-adapter.ts +4 -4
  167. package/src/providers/codex-models.ts +65 -198
  168. package/src/providers/reasoning-effort.ts +44 -140
  169. package/src/rbac/legacy-policy.ts +25 -0
  170. package/src/rbac/permissions.ts +10 -0
  171. package/src/scripts-runtime/sdk-allowlist.ts +4 -0
  172. package/src/scripts-runtime/types/stdlib.d.ts +15 -0
  173. package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
  174. package/src/scripts-runtime/types/swarm-sdk.d.ts +15 -0
  175. package/src/server.ts +6 -0
  176. package/src/slack/message-text.ts +56 -2
  177. package/src/slack/thread-buffer.ts +10 -2
  178. package/src/tests/agents-list-model-display.test.ts +4 -6
  179. package/src/tests/base-prompt.test.ts +3 -0
  180. package/src/tests/claude-adapter.test.ts +10 -0
  181. package/src/tests/claude-managed-adapter.test.ts +12 -18
  182. package/src/tests/codex-adapter.test.ts +2 -1
  183. package/src/tests/codex-gpt6-release.test.ts +66 -5
  184. package/src/tests/context-window.test.ts +7 -5
  185. package/src/tests/db-extraction-contract.test.ts +1 -0
  186. package/src/tests/db-task-read-contract.test.ts +5 -1
  187. package/src/tests/db-task-write-contract.test.ts +4 -1
  188. package/src/tests/docs-index-footprint.test.js +3 -3
  189. package/src/tests/explicit-model-validation.test.ts +332 -0
  190. package/src/tests/harness-model-support.test.ts +374 -0
  191. package/src/tests/heartbeat-checklist-config.test.ts +71 -0
  192. package/src/tests/heartbeat-checklist.test.ts +4 -2
  193. package/src/tests/heartbeat-orphan-resume.test.ts +48 -0
  194. package/src/tests/human-free-flag.test.ts +279 -0
  195. package/src/tests/human-free-oracle.ts +57 -0
  196. package/src/tests/human-free-reclassify.test.ts +669 -0
  197. package/src/tests/migration-046-budgets.test.ts +3 -3
  198. package/src/tests/model-catalog-migration.test.ts +134 -0
  199. package/src/tests/model-catalog-table.test.ts +249 -0
  200. package/src/tests/model-catalog-write-access.test.ts +387 -0
  201. package/src/tests/model-tier-resolution.test.ts +519 -0
  202. package/src/tests/models-catalog.test.ts +1 -1
  203. package/src/tests/multi-runtime-enabled-stats.test.ts +65 -1
  204. package/src/tests/package-publish.test.ts +137 -36
  205. package/src/tests/pricing-routes.test.ts +2 -2
  206. package/src/tests/rbac-engine.test.ts +42 -2
  207. package/src/tests/reasoning-effort.test.ts +109 -0
  208. package/src/tests/refresh-modelsdev-carryforward.test.ts +5 -5
  209. package/src/tests/runtime-model-catalog.test.ts +86 -0
  210. package/src/tests/script-connections.test.ts +5 -0
  211. package/src/tests/seed-scripts.test.ts +89 -0
  212. package/src/tests/session-costs.test.ts +14 -11
  213. package/src/tests/slack-message-text.test.ts +101 -0
  214. package/src/tests/slack-thread-buffer-context.test.ts +93 -0
  215. package/src/tests/task-cascade-fail.test.ts +148 -2
  216. package/src/tests/tool-annotations.test.ts +1 -1
  217. package/src/tests/usage-cache.test.ts +166 -0
  218. package/src/tests/workflow-executors.test.ts +5 -2
  219. package/src/tests/workflow-system-one-decision-review.test.ts +1021 -0
  220. package/src/tests/workflow-system-one-decision-save-warnings.test.ts +365 -0
  221. package/src/tests/workflow-system-one-decision.test.ts +2063 -0
  222. package/src/tools/model-catalog/index.ts +137 -0
  223. package/src/tools/schedules/create-schedule.ts +16 -1
  224. package/src/tools/schedules/patch-schedule.ts +17 -0
  225. package/src/tools/schedules/update-schedule.ts +17 -0
  226. package/src/tools/send-task.ts +14 -0
  227. package/src/tools/task-action.ts +12 -0
  228. package/src/tools/tool-config.ts +3 -1
  229. package/src/tools/workflows/create-workflow.ts +9 -2
  230. package/src/tools/workflows/patch-workflow-node.ts +17 -7
  231. package/src/tools/workflows/patch-workflow.ts +9 -2
  232. package/src/tools/workflows/update-workflow.ts +9 -2
  233. package/src/types.ts +30 -1
  234. package/src/utils/context-window.ts +47 -53
  235. package/src/utils/harness-cli-version.ts +115 -0
  236. package/src/utils/harness-model-error.ts +33 -0
  237. package/src/utils/runtime-model-catalog.ts +137 -0
  238. package/src/workflows/approval-resolution.ts +39 -0
  239. package/src/workflows/definition.ts +7 -0
  240. package/src/workflows/engine.ts +69 -1
  241. package/src/workflows/executors/agent-task.ts +9 -0
  242. package/src/workflows/executors/base.ts +53 -0
  243. package/src/workflows/executors/human-in-the-loop.ts +8 -8
  244. package/src/workflows/executors/index.ts +1 -0
  245. package/src/workflows/executors/registry.ts +2 -0
  246. package/src/workflows/executors/system-one-decision.ts +1232 -0
  247. package/src/workflows/executors/system-one-providers.ts +301 -0
  248. package/src/workflows/executors/workflow-llm.ts +13 -1
  249. package/src/workflows/readiness.ts +86 -0
  250. package/src/workflows/recovery.ts +12 -12
  251. package/src/workflows/resume.ts +23 -9
  252. package/templates/schedules/daily-blocker-digest/content.md +4 -5
  253. package/templates/skills/agent-browser/SKILL.md +6 -5
  254. package/templates/skills/agent-browser/content.md +6 -5
  255. package/templates/skills/heartbeat-runbook/config.json +1 -1
  256. package/templates/skills/heartbeat-runbook/content.md +2 -2
  257. package/templates/skills/swarm-scripts/SKILL.md +1 -1
  258. package/templates/skills/swarm-scripts/content.md +1 -1
  259. package/templates/skills/workflow-iterate/content.md +81 -1
  260. package/templates/workflows/claude-code-changelog-watch/content.md +2 -0
  261. package/templates/workflows/pr-review-status-sweep/content.md +2 -0
  262. package/dist/claude-sdk-session-t442qz6f.js +0 -39108
  263. package/dist/cli-0xjkj1j7.js +0 -61
  264. package/dist/cli-65hjmev5.js +0 -1141
  265. package/dist/cli-aw1jwp1r.js +0 -14075
  266. package/dist/cli-h967nb6r.js +0 -6603
  267. package/dist/cli-vapyk8sw.js +0 -20
  268. package/dist/cli-ws6rqycm.js +0 -247
  269. package/dist/pricing-refresh-v1cbr9te.js +0 -27
  270. package/src/providers/modelsdev-reasoning.json +0 -6453
  271. package/dist/{codex-login-x3jk4etn.js → codex-login-he9vrn90.js} +3 -3
  272. package/dist/{index-f24115ge.js → index-2wsdsnmm.js} +3 -3
@@ -15,7 +15,17 @@ import { scrubSecrets } from "../../../utils/secret-scrubber";
15
15
  import { emitTaskStarted } from "../../task-lifecycle-events";
16
16
  import { getAgentById, isAgentEligibleForTask } from "../agents";
17
17
  import { getDbClient } from "../runtime";
18
- import { type AgentTaskRow, getTaskById, rowToAgentTask } from "./read";
18
+ import {
19
+ classifyTaskHumanFree,
20
+ reclassifyTaskHumanFree,
21
+ tagWriteChangesHumanFree,
22
+ } from "./human-free";
23
+ import {
24
+ type AgentTaskRow,
25
+ getTaskById,
26
+ NEVER_STARTED_TASK_STATUSES,
27
+ rowToAgentTask,
28
+ } from "./read";
19
29
 
20
30
  type TaskWriteDependencies = {
21
31
  createLogEntry: (entry: {
@@ -68,9 +78,10 @@ export async function createTask(
68
78
  ): Promise<AgentTask> {
69
79
  const id = crypto.randomUUID();
70
80
  const source = options?.source ?? "mcp";
81
+ const isHumanFree = await classifyTaskHumanFree({ source });
71
82
  const row = await getDbClient().get<AgentTaskRow>(
72
- `INSERT INTO agent_tasks (id, "key", agentId, task, status, source, slackChannelId, slackThreadTs, slackUserId, swarmVersion, createdAt, lastUpdatedAt)
73
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) RETURNING *`,
83
+ `INSERT INTO agent_tasks (id, "key", agentId, task, status, source, slackChannelId, slackThreadTs, slackUserId, swarmVersion, isHumanFree, createdAt, lastUpdatedAt)
84
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) RETURNING *`,
74
85
  [
75
86
  id,
76
87
  defaultAssetKey("task", id),
@@ -82,6 +93,7 @@ export async function createTask(
82
93
  options?.slackThreadTs ?? null,
83
94
  options?.slackUserId ?? null,
84
95
  pkg.version,
96
+ isHumanFree ? 1 : 0,
85
97
  ],
86
98
  );
87
99
  if (!row) throw new Error("Failed to create task");
@@ -359,10 +371,17 @@ export async function completeTask(
359
371
  if (completed && options?.addTags?.length) {
360
372
  const existingTags: string[] = completed.tags ? JSON.parse(completed.tags) : [];
361
373
  const nextTags = Array.from(new Set([...existingTags, ...options.addTags]));
374
+ const previousTagsJson = completed.tags;
375
+ const nextTagsJson = JSON.stringify(nextTags);
362
376
  completed = await getDbClient().get<AgentTaskRow>(
363
377
  "UPDATE agent_tasks SET tags = ?, lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ? RETURNING *",
364
- [JSON.stringify(nextTags), id],
378
+ [nextTagsJson, id],
365
379
  );
380
+ // Only a tag the classifier reads (e.g. "heartbeat") can flip the flag;
381
+ // a "deferred" write must not walk the task's whole subtree.
382
+ if (tagWriteChangesHumanFree(previousTagsJson, nextTagsJson)) {
383
+ await reclassifyTaskHumanFree([id]);
384
+ }
366
385
  }
367
386
  if (completed && options?.deferredAt) {
368
387
  completed = await getDbClient().get<AgentTaskRow>(
@@ -752,20 +771,88 @@ export async function supersedeTask(
752
771
  );
753
772
  });
754
773
 
755
- try {
756
- await dependencies.cascadeFailDependents(id, "superseded");
757
- } catch (err) {
758
- console.error("[supersedeTask] cascade-fail dependents error:", err);
759
- }
774
+ // Without a resume id the dependents wait: every caller supersedes first,
775
+ // creates the resume, then calls `backfillSupersedeTaskResumeTaskId`, which
776
+ // settles them. Cascading here would fail work the resume is about to carry.
777
+ // A caller whose resume is not created settles with `null`; heartbeat
778
+ // repair settles whatever a crash left in between.
779
+ if (args.resumeTaskId) await settleSupersededTaskDependents(id, args.resumeTaskId);
760
780
  }
761
781
 
762
782
  return row ? rowToAgentTask(row) : null;
763
783
  }
764
784
 
785
+ /**
786
+ * Settle the dependents of a superseded task. With a resume id, every
787
+ * never-started dependent has the superseded id in `dependsOn` replaced by the
788
+ * resume id, so it waits on the resume instead of dying with
789
+ * `Blocked dependency … was superseded`. Anything still depending on the
790
+ * superseded task afterwards (a dependent that already started, or every
791
+ * dependent when there is no resume) cascade-fails. Idempotent.
792
+ */
793
+ export async function settleSupersededTaskDependents(
794
+ supersededId: string,
795
+ resumeTaskId: string | null,
796
+ ): Promise<void> {
797
+ try {
798
+ if (resumeTaskId) {
799
+ const placeholders = NEVER_STARTED_TASK_STATUSES.map(() => "?").join(", ");
800
+ // One statement: the new array is built from the row's current
801
+ // `dependsOn`, so settlements of a shared dependent compose, and the
802
+ // status predicate is evaluated at write time, so a claim into
803
+ // `in_progress` is skipped while offered -> reviewing/pending is not.
804
+ const repointed = await getDbClient().query<{ id: string; agentId: string | null }>(
805
+ `UPDATE agent_tasks
806
+ SET dependsOn = (
807
+ SELECT json_group_array(value ORDER BY k) FROM (
808
+ SELECT CASE WHEN j.value = ? THEN ? ELSE j.value END AS value,
809
+ MIN(j.key) AS k
810
+ FROM json_each(agent_tasks.dependsOn) AS j
811
+ GROUP BY 1)),
812
+ lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
813
+ WHERE status IN (${placeholders})
814
+ AND EXISTS (SELECT 1 FROM json_each(agent_tasks.dependsOn) WHERE value = ?)
815
+ RETURNING id, agentId`,
816
+ [supersededId, resumeTaskId, ...NEVER_STARTED_TASK_STATUSES, supersededId],
817
+ );
818
+ for (const dep of repointed) {
819
+ try {
820
+ await dependencies.createLogEntry({
821
+ eventType: "task_dependency_repointed",
822
+ taskId: dep.id,
823
+ agentId: dep.agentId ?? undefined,
824
+ oldValue: supersededId,
825
+ newValue: resumeTaskId,
826
+ metadata: { reason: "supersede_resume" },
827
+ });
828
+ } catch {}
829
+ }
830
+ // Repair can settle onto a resume that already failed; its own cascade
831
+ // ran before these dependents pointed at it.
832
+ const resume = await getTaskById(resumeTaskId);
833
+ if (repointed.length > 0 && (resume?.status === "failed" || resume?.status === "cancelled")) {
834
+ await dependencies.cascadeFailDependents(resumeTaskId, resume.status);
835
+ }
836
+ }
837
+ await dependencies.cascadeFailDependents(supersededId, "superseded");
838
+ } catch (err) {
839
+ console.error("[supersedeTask] settling dependents error:", err);
840
+ }
841
+ }
842
+
843
+ /**
844
+ * Attach the resume id to the task's `task_superseded` log entry, then settle
845
+ * the dependents `supersedeTask` left waiting (re-point to the resume, cascade
846
+ * the rest).
847
+ */
765
848
  export async function backfillSupersedeTaskResumeTaskId(
766
849
  taskId: string,
767
850
  resumeTaskId: string,
768
851
  ): Promise<boolean> {
852
+ if ((await getTaskById(taskId))?.status === "superseded") {
853
+ await settleSupersededTaskDependents(taskId, resumeTaskId);
854
+ }
855
+
769
856
  const row = await getDbClient().get<{ id: string; metadata: string | null }>(
770
857
  `SELECT id, metadata
771
858
  FROM agent_log
@@ -960,8 +1047,17 @@ export async function getRecentlyCancelledTasksForAgent(agentId: string): Promis
960
1047
  }
961
1048
 
962
1049
  export async function deleteTask(id: string): Promise<boolean> {
963
- const result = await getDbClient().run("DELETE FROM agent_tasks WHERE id = ?", [id]);
964
- return result.changes > 0;
1050
+ return await getDbClient().transaction(async (tx) => {
1051
+ // `parentTaskId` has no foreign key, so children survive with a dangling
1052
+ // parent. The human-free rule reads the parent row, so they are reclassified
1053
+ // once it is gone.
1054
+ const childIds = (
1055
+ await tx.query<{ id: string }>("SELECT id FROM agent_tasks WHERE parentTaskId = ?", [id])
1056
+ ).map((row) => row.id);
1057
+ const result = await tx.run("DELETE FROM agent_tasks WHERE id = ?", [id]);
1058
+ await reclassifyTaskHumanFree(childIds);
1059
+ return result.changes > 0;
1060
+ });
965
1061
  }
966
1062
 
967
1063
  export async function updateTaskProgress(id: string, progress: string): Promise<AgentTask | null> {
package/src/be/db.ts CHANGED
@@ -161,6 +161,7 @@ import {
161
161
  getLatestContextVersion,
162
162
  } from "./db/context-versions";
163
163
  import { getDb, getDbClient } from "./db/runtime";
164
+ import { classifyTaskHumanFree, reclassifyTaskHumanFree } from "./db/tasks/human-free";
164
165
  import {
165
166
  type AgentTaskRow,
166
167
  configureTaskReadDependencies,
@@ -187,6 +188,7 @@ export {
187
188
  getAgentById,
188
189
  getAgentDailyTaskCounts,
189
190
  getAgentHarnessProviders,
191
+ getAgentStatusCounts,
190
192
  getAllAgents,
191
193
  getLeadAgent,
192
194
  getRemainingCapacity,
@@ -260,6 +262,7 @@ export {
260
262
  getRecentlyFinishedWorkerTasks,
261
263
  getSlackTasksMissingTree,
262
264
  getSupersededTasksWithoutResume,
265
+ getSupersededTasksWithUnsettledDependents,
263
266
  getTaskById,
264
267
  getTaskStats,
265
268
  getTasksByAgentId,
@@ -291,6 +294,7 @@ export {
291
294
  pauseTask,
292
295
  resetOrphanedInProgressTasksForAgent,
293
296
  resumeTask,
297
+ settleSupersededTaskDependents,
294
298
  startTask,
295
299
  supersedeTask,
296
300
  updateTaskClaudeSessionId,
@@ -2703,6 +2707,16 @@ export async function createTaskExtended(
2703
2707
  logRoutingDecisionFailure(id, "capture", error);
2704
2708
  }
2705
2709
 
2710
+ const isHumanFree = await classifyTaskHumanFree({
2711
+ taskType: options?.taskType,
2712
+ tags: JSON.stringify(options?.tags ?? []),
2713
+ source: options?.source ?? "mcp",
2714
+ requestedByUserId: options?.requestedByUserId,
2715
+ requestedByUserIdInherited,
2716
+ parentTaskId: options?.parentTaskId,
2717
+ workflowRunId: options?.workflowRunId,
2718
+ });
2719
+
2706
2720
  const inserted = await getDbClient().get<AgentTaskRow>(
2707
2721
  `INSERT INTO agent_tasks (
2708
2722
  id, "key", agentId, creatorAgentId, task, status, source, routing_reason, routing_source, routing_note,
@@ -2712,8 +2726,8 @@ export async function createTaskExtended(
2712
2726
  vcsInstallationId, vcsNodeId,
2713
2727
  agentmailInboxId, agentmailMessageId, agentmailThreadId,
2714
2728
  mentionMessageId, mentionChannelId, dir, parentTaskId, model, modelTier, effort, scheduleId,
2715
- workflowRunId, workflowRunStepId, outputSchema, followUpConfig, requestedByUserId, requestedByUserIdInherited, contextKey, routingAffinity, swarmVersion, createdAt, lastUpdatedAt, created_by, updated_by
2716
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
2729
+ workflowRunId, workflowRunStepId, outputSchema, followUpConfig, requestedByUserId, requestedByUserIdInherited, contextKey, routingAffinity, swarmVersion, createdAt, lastUpdatedAt, created_by, updated_by, isHumanFree
2730
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
2717
2731
  [
2718
2732
  id,
2719
2733
  assetKey,
@@ -2768,6 +2782,7 @@ export async function createTaskExtended(
2768
2782
  now,
2769
2783
  auditUserId,
2770
2784
  auditUserId,
2785
+ isHumanFree ? 1 : 0,
2771
2786
  ],
2772
2787
  );
2773
2788
  if (!inserted) throw new Error("Failed to create task");
@@ -4893,60 +4908,12 @@ export interface SessionCostByUserRow {
4893
4908
  /** `opts.userId` sentinel selecting spend with no human requester. */
4894
4909
  export const UNATTRIBUTED_USER_ID = "unattributed";
4895
4910
 
4896
- // Structurally-human-free tasks and their descendants. An explicitly supplied
4897
- // requester is a human handoff and stops propagation; a requester copied from
4898
- // the parent does not.
4899
- const HUMAN_FREE_TASKS_CTE = `human_free_tasks(id) AS (
4900
- SELECT task.id
4901
- FROM agent_tasks task
4902
- LEFT JOIN agent_tasks parent ON parent.id = task.parentTaskId
4903
- WHERE COALESCE(task.taskType, '') IN ('heartbeat', 'heartbeat-checklist', 'boot-triage')
4904
- OR COALESCE(task.tags, '[]') LIKE '%"heartbeat"%'
4905
- OR (COALESCE(task.source, '') = 'schedule' AND task.requestedByUserId IS NULL)
4906
- OR (
4907
- task.parentTaskId IS NULL
4908
- AND COALESCE(task.source, '') = 'workflow'
4909
- AND task.requestedByUserId IS NULL
4910
- AND EXISTS (
4911
- SELECT 1
4912
- FROM workflow_runs run
4913
- WHERE run.id = task.workflowRunId
4914
- AND run.triggerType = 'schedule'
4915
- AND run.created_by IS NULL
4916
- )
4917
- )
4918
- OR (
4919
- COALESCE(task.source, '') = 'system'
4920
- AND parent.id IS NOT NULL
4921
- AND parent.requestedByUserId IS NULL
4922
- )
4923
-
4924
- UNION
4925
-
4926
- SELECT child.id
4927
- FROM agent_tasks child
4928
- JOIN human_free_tasks parent ON child.parentTaskId = parent.id
4929
- WHERE child.requestedByUserId IS NULL
4930
- OR child.requestedByUserIdInherited = 1
4931
- )`;
4932
- /** True on rows joined through `human_free_tasks hf` (see `getSessionCostSummary`). */
4933
- const HUMAN_FREE_SQL = "hf.id IS NOT NULL";
4934
- const ROOT_HUMAN_FREE_SQL = `(
4935
- COALESCE(t.taskType, '') IN ('heartbeat', 'heartbeat-checklist', 'boot-triage')
4936
- OR COALESCE(t.tags, '[]') LIKE '%"heartbeat"%'
4937
- OR (COALESCE(t.source, '') = 'schedule' AND t.requestedByUserId IS NULL)
4938
- OR (
4939
- COALESCE(t.source, '') = 'workflow'
4940
- AND t.requestedByUserId IS NULL
4941
- AND EXISTS (
4942
- SELECT 1
4943
- FROM workflow_runs run
4944
- WHERE run.id = t.workflowRunId
4945
- AND run.triggerType = 'schedule'
4946
- AND run.created_by IS NULL
4947
- )
4948
- )
4949
- )`;
4911
+ /**
4912
+ * True for a session whose task is structurally human-free (`isHumanFree`, set
4913
+ * when the task is created; rule in `src/be/db/tasks/human-free.ts`). A session
4914
+ * with no task row is not human-free.
4915
+ */
4916
+ const HUMAN_FREE_SQL = "(COALESCE(t.isHumanFree, 0) = 1)";
4950
4917
 
4951
4918
  export async function getSessionCostSummary(opts: {
4952
4919
  startDate?: string;
@@ -4966,19 +4933,15 @@ export async function getSessionCostSummary(opts: {
4966
4933
  // re-attributed after the fact, so the human requester is resolved by joining
4967
4934
  // through the task (same shape as `getDailySpendForUser`). Every column is
4968
4935
  // `sc.`-qualified because `createdAt`/`agentId` exist on both sides.
4969
- const from = "FROM session_costs sc LEFT JOIN agent_tasks t ON t.id = sc.taskId";
4970
- // One join marks the sessions of structurally-human-free tasks (`hf.id` set).
4971
- // The CTE holds each task id once, so the join never duplicates a session.
4972
- const fromHf = `${from} LEFT JOIN human_free_tasks hf ON hf.id = t.id`;
4973
-
4974
- // Structurally-human-free: the swarm maintaining itself, with no human
4975
- // requester by construction — heartbeat/boot-triage tasks, scheduled runs
4976
- // without a human creator (including workflow roots launched by such a
4977
- // schedule), and `source='system'` follow-ups whose parent itself has no
4978
- // human requester. That classification propagates through descendants while
4979
- // they remain unattributed, so autonomous fan-out cannot leak back into the
4980
- // denominator. An explicitly attributed child is an independent handoff and
4981
- // stops propagation down that branch.
4936
+ // INDEXED BY: without ANALYZE statistics (production has none) the planner
4937
+ // probes tasks through the primary key and reads each full row, which follows
4938
+ // the task's overflow pages. The covering index answers the same probe from
4939
+ // the index alone (migration 182).
4940
+ const from =
4941
+ "FROM session_costs sc LEFT JOIN agent_tasks t INDEXED BY idx_agent_tasks_usage_cover ON t.id = sc.taskId";
4942
+ // Only a user filter reads the task, and the join adds no rows (task id is unique).
4943
+ const agentFrom = opts.userId === undefined ? "FROM session_costs sc" : from;
4944
+
4982
4945
  const conditions: string[] = [];
4983
4946
  const params: string[] = [];
4984
4947
 
@@ -5002,11 +4965,6 @@ export async function getSessionCostSummary(opts: {
5002
4965
  }
5003
4966
 
5004
4967
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5005
- // Only a user filter makes the daily and per-agent breakdowns classify rows.
5006
- // Without one they skip the CTE, which scans every task.
5007
- const classify = opts.userId !== undefined;
5008
- const withHf = classify ? `WITH RECURSIVE ${HUMAN_FREE_TASKS_CTE}` : "";
5009
- const scopedFrom = classify ? fromHf : from;
5010
4968
  const subscriptionSql = `t.credentialKeyType IN (${SUBSCRIPTION_KEY_TYPES.map((k) => `'${k}'`).join(", ")})`;
5011
4969
 
5012
4970
  // Totals
@@ -5026,8 +4984,7 @@ export async function getSessionCostSummary(opts: {
5026
4984
  };
5027
4985
 
5028
4986
  const totalsRow = await getDbClient().get<TotalsRow>(
5029
- `WITH RECURSIVE ${HUMAN_FREE_TASKS_CTE}
5030
- SELECT
4987
+ `SELECT
5031
4988
  COALESCE(SUM(sc.totalCostUsd), 0) as totalCostUsd,
5032
4989
  COALESCE(SUM(sc.inputTokens), 0) as totalInputTokens,
5033
4990
  COALESCE(SUM(sc.outputTokens), 0) as totalOutputTokens,
@@ -5044,7 +5001,7 @@ export async function getSessionCostSummary(opts: {
5044
5001
  COUNT(DISTINCT CASE WHEN ${HUMAN_FREE_SQL} THEN t.id END) as excludedTaskCount,
5045
5002
  COALESCE(SUM(CASE WHEN ${subscriptionSql}
5046
5003
  THEN sc.totalCostUsd ELSE 0 END), 0) as subscriptionCostUsd
5047
- ${fromHf} ${where}`,
5004
+ ${from} ${where}`,
5048
5005
  params,
5049
5006
  );
5050
5007
 
@@ -5082,8 +5039,7 @@ export async function getSessionCostSummary(opts: {
5082
5039
  sessions: number;
5083
5040
  subscriptionCostUsd: number;
5084
5041
  }>(
5085
- `${withHf}
5086
- SELECT
5042
+ `SELECT
5087
5043
  DATE(sc.createdAt) as date,
5088
5044
  COALESCE(SUM(sc.totalCostUsd), 0) as costUsd,
5089
5045
  COALESCE(SUM(sc.inputTokens), 0) as inputTokens,
@@ -5091,7 +5047,7 @@ export async function getSessionCostSummary(opts: {
5091
5047
  COUNT(*) as sessions,
5092
5048
  COALESCE(SUM(CASE WHEN ${subscriptionSql} THEN sc.totalCostUsd ELSE 0 END), 0)
5093
5049
  as subscriptionCostUsd
5094
- ${scopedFrom} ${where}
5050
+ ${from} ${where}
5095
5051
  GROUP BY DATE(sc.createdAt)
5096
5052
  ORDER BY date ASC`,
5097
5053
  params,
@@ -5109,15 +5065,14 @@ export async function getSessionCostSummary(opts: {
5109
5065
  sessions: number;
5110
5066
  durationMs: number;
5111
5067
  }>(
5112
- `${withHf}
5113
- SELECT
5068
+ `SELECT
5114
5069
  sc.agentId as agentId,
5115
5070
  COALESCE(SUM(sc.totalCostUsd), 0) as costUsd,
5116
5071
  COALESCE(SUM(sc.inputTokens), 0) as inputTokens,
5117
5072
  COALESCE(SUM(sc.outputTokens), 0) as outputTokens,
5118
5073
  COUNT(*) as sessions,
5119
5074
  COALESCE(SUM(sc.durationMs), 0) as durationMs
5120
- ${scopedFrom} ${where}
5075
+ ${agentFrom} ${where}
5121
5076
  GROUP BY sc.agentId
5122
5077
  ORDER BY costUsd DESC`,
5123
5078
  params,
@@ -5129,15 +5084,14 @@ export async function getSessionCostSummary(opts: {
5129
5084
  let byUser: SessionCostByUserRow[] = [];
5130
5085
  if (groupBy === "user" || groupBy === "both") {
5131
5086
  byUser = await getDbClient().query<SessionCostByUserRow>(
5132
- `WITH RECURSIVE ${HUMAN_FREE_TASKS_CTE}
5133
- SELECT
5087
+ `SELECT
5134
5088
  CASE WHEN ${HUMAN_FREE_SQL} THEN NULL ELSE t.requestedByUserId END as userId,
5135
5089
  COALESCE(SUM(sc.totalCostUsd), 0) as costUsd,
5136
5090
  COALESCE(SUM(sc.inputTokens), 0) as inputTokens,
5137
5091
  COALESCE(SUM(sc.outputTokens), 0) as outputTokens,
5138
5092
  COUNT(DISTINCT sc.taskId) as tasks,
5139
5093
  COALESCE(SUM(sc.durationMs), 0) as durationMs
5140
- ${fromHf} ${where}
5094
+ ${from} ${where}
5141
5095
  GROUP BY CASE WHEN ${HUMAN_FREE_SQL} THEN NULL ELSE t.requestedByUserId END
5142
5096
  ORDER BY costUsd DESC`,
5143
5097
  params,
@@ -5151,8 +5105,7 @@ export async function getSessionCostSummary(opts: {
5151
5105
  "name" | "subscription" | "plan" | "planSource"
5152
5106
  >;
5153
5107
  const rows = await getDbClient().query<CredentialRow>(
5154
- `${withHf}
5155
- SELECT
5108
+ `SELECT
5156
5109
  t.credentialKeyType as keyType,
5157
5110
  t.credentialKeySuffix as keySuffix,
5158
5111
  COALESCE(SUM(sc.totalCostUsd), 0) as costUsd,
@@ -5161,7 +5114,7 @@ export async function getSessionCostSummary(opts: {
5161
5114
  COUNT(*) as sessions,
5162
5115
  MIN(sc.createdAt) as firstSessionAt,
5163
5116
  MAX(sc.createdAt) as lastSessionAt
5164
- ${scopedFrom} ${where}
5117
+ ${from} ${where}
5165
5118
  GROUP BY t.credentialKeyType, t.credentialKeySuffix
5166
5119
  ORDER BY costUsd DESC`,
5167
5120
  params,
@@ -5223,18 +5176,6 @@ async function getCredentialLabels(): Promise<
5223
5176
  return labels;
5224
5177
  }
5225
5178
 
5226
- /**
5227
- * Changes when a session cost or a task is inserted. Part of the usage report
5228
- * cache key (`src/http/usage-cache.ts`). Reads two rowid maxima, so it is cheap.
5229
- */
5230
- export async function getUsageDataVersion(): Promise<string> {
5231
- const row = await getDbClient().get<{ version: string }>(
5232
- `SELECT COALESCE((SELECT MAX(rowid) FROM session_costs), 0) || ':' ||
5233
- COALESCE((SELECT MAX(rowid) FROM agent_tasks), 0) as version`,
5234
- );
5235
- return row?.version ?? "0:0";
5236
- }
5237
-
5238
5179
  // --- Per-person attribution (four-metric view) ---
5239
5180
 
5240
5181
  export interface AttributionByPersonRow {
@@ -5265,7 +5206,7 @@ export interface AttributionByPersonRow {
5265
5206
  * a composite score. Scope is root tasks (`problemsInitiated`/`problemsShipped`)
5266
5207
  * or the person's entire task tree (`*Reached`); `requestedByUserId IS NULL`
5267
5208
  * (autonomous work) and structurally-human-free rows with a stale/inherited
5268
- * requester (see `HUMAN_FREE_SQL` in `getSessionCostSummary`) are excluded —
5209
+ * requester (`agent_tasks.isHumanFree`, see `src/be/db/tasks/human-free.ts`) are excluded —
5269
5210
  * neither belongs to a person.
5270
5211
  *
5271
5212
  * "Problems shipped" detection walks each root's full task tree, preferring a
@@ -5309,18 +5250,20 @@ export async function getAttributionByPerson(opts: {
5309
5250
 
5310
5251
  type RootRow = { userId: string; initiated: number; shipped: number };
5311
5252
  const rootRows = await getDbClient().query<RootRow>(
5312
- `WITH RECURSIVE selected_roots(id, requestedByUserId, status, output) AS (
5313
- SELECT t.id, t.requestedByUserId, t.status, t.output
5253
+ `WITH RECURSIVE selected_roots(id, requestedByUserId, status) AS (
5254
+ SELECT t.id, t.requestedByUserId, t.status
5314
5255
  FROM agent_tasks t
5315
- ${where} AND t.parentTaskId IS NULL AND NOT ${ROOT_HUMAN_FREE_SQL}
5256
+ ${where} AND t.parentTaskId IS NULL AND t.isHumanFree = 0
5316
5257
  ),
5317
- task_tree(rootId, taskId, output) AS (
5318
- SELECT id, id, output
5258
+ -- Ids only: carrying each descendant's \`output\` through the recursion copied
5259
+ -- every task's text into the queue. The output fallback below reads it by id.
5260
+ task_tree(rootId, taskId) AS (
5261
+ SELECT id, id
5319
5262
  FROM selected_roots
5320
5263
 
5321
5264
  UNION ALL
5322
5265
 
5323
- SELECT tree.rootId, child.id, child.output
5266
+ SELECT tree.rootId, child.id
5324
5267
  FROM agent_tasks child
5325
5268
  JOIN task_tree tree ON child.parentTaskId = tree.taskId
5326
5269
  ),
@@ -5353,8 +5296,11 @@ export async function getAttributionByPerson(opts: {
5353
5296
 
5354
5297
  SELECT tree.rootId
5355
5298
  FROM task_tree tree
5356
- WHERE tree.output LIKE '%github.com/%/pull/%'
5357
- OR tree.output LIKE '%/-/merge_requests/%'
5299
+ -- CROSS JOIN pins the order (tree outer, task by id). Left to itself the
5300
+ -- planner scans every task's output and probes the tree instead.
5301
+ CROSS JOIN agent_tasks described ON described.id = tree.taskId
5302
+ WHERE described.output LIKE '%github.com/%/pull/%'
5303
+ OR described.output LIKE '%/-/merge_requests/%'
5358
5304
  )
5359
5305
  SELECT
5360
5306
  t.requestedByUserId as userId,
@@ -5373,65 +5319,13 @@ export async function getAttributionByPerson(opts: {
5373
5319
  surfacesReached: number;
5374
5320
  };
5375
5321
  const reachRows = await getDbClient().query<ReachRow>(
5376
- `WITH RECURSIVE report_tasks AS (
5377
- -- Only the columns read below: a task row carries its full prompt and output.
5378
- SELECT t.id, t.agentId, t.vcsRepo, t.source, t.parentTaskId, t.requestedByUserId,
5379
- t.requestedByUserIdInherited, t.taskType, t.tags, t.workflowRunId
5380
- FROM agent_tasks t
5381
- ${where}
5382
- ),
5383
- task_ancestry(
5384
- taskId, id, parentTaskId, requestedByUserId, requestedByUserIdInherited,
5385
- taskType, tags, source, workflowRunId
5386
- ) AS (
5387
- SELECT
5388
- id, id, parentTaskId, requestedByUserId, requestedByUserIdInherited,
5389
- taskType, tags, source, workflowRunId
5390
- FROM report_tasks
5391
-
5392
- UNION ALL
5393
-
5394
- SELECT
5395
- child.taskId, parent.id, parent.parentTaskId, parent.requestedByUserId,
5396
- parent.requestedByUserIdInherited, parent.taskType, parent.tags,
5397
- parent.source, parent.workflowRunId
5398
- FROM agent_tasks parent
5399
- JOIN task_ancestry child ON parent.id = child.parentTaskId
5400
- WHERE child.requestedByUserId IS NULL
5401
- OR child.requestedByUserIdInherited = 1
5402
- ),
5403
- human_free_report_tasks(id) AS (
5404
- SELECT DISTINCT ancestor.taskId
5405
- FROM task_ancestry ancestor
5406
- LEFT JOIN agent_tasks parent ON parent.id = ancestor.parentTaskId
5407
- WHERE COALESCE(ancestor.taskType, '') IN ('heartbeat', 'heartbeat-checklist', 'boot-triage')
5408
- OR COALESCE(ancestor.tags, '[]') LIKE '%"heartbeat"%'
5409
- OR (COALESCE(ancestor.source, '') = 'schedule' AND ancestor.requestedByUserId IS NULL)
5410
- OR (
5411
- ancestor.parentTaskId IS NULL
5412
- AND COALESCE(ancestor.source, '') = 'workflow'
5413
- AND ancestor.requestedByUserId IS NULL
5414
- AND EXISTS (
5415
- SELECT 1
5416
- FROM workflow_runs run
5417
- WHERE run.id = ancestor.workflowRunId
5418
- AND run.triggerType = 'schedule'
5419
- AND run.created_by IS NULL
5420
- )
5421
- )
5422
- OR (
5423
- COALESCE(ancestor.source, '') = 'system'
5424
- AND parent.id IS NOT NULL
5425
- AND parent.requestedByUserId IS NULL
5426
- )
5427
- )
5428
- SELECT
5322
+ `SELECT
5429
5323
  t.requestedByUserId as userId,
5430
5324
  COUNT(DISTINCT t.agentId) as agentsReached,
5431
5325
  COUNT(DISTINCT t.vcsRepo) as reposReached,
5432
5326
  COUNT(DISTINCT t.source) as surfacesReached
5433
- FROM report_tasks t
5434
- WHERE NOT EXISTS (SELECT 1 FROM human_free_report_tasks WHERE id = t.id)
5327
+ FROM agent_tasks t
5328
+ ${where} AND t.isHumanFree = 0
5435
5329
  GROUP BY t.requestedByUserId`,
5436
5330
  params,
5437
5331
  );
@@ -7596,7 +7490,19 @@ export async function updateWorkflow(
7596
7490
  }
7597
7491
 
7598
7492
  export async function deleteWorkflow(id: string, source?: "api" | "mcp"): Promise<boolean> {
7493
+ // One transaction: the stored human-free flags must never lag the deleted runs
7494
+ // (a scheduled workflow root is human-free only while its run row exists).
7495
+ return await getDbClient().transaction(() => deleteWorkflowRows(id, source));
7496
+ }
7497
+
7498
+ async function deleteWorkflowRows(id: string, source?: "api" | "mcp"): Promise<boolean> {
7599
7499
  const client = getDbClient();
7500
+ const linkedTaskIds = (
7501
+ await client.query<{ id: string }>(
7502
+ `SELECT id FROM agent_tasks WHERE workflowRunId IN (SELECT id FROM workflow_runs WHERE workflowId = ?)`,
7503
+ [id],
7504
+ )
7505
+ ).map((row) => row.id);
7600
7506
  // Cascade delete in FK-safe order:
7601
7507
  // 1. Unlink agent_tasks (they reference steps and runs)
7602
7508
  await client.run(
@@ -7610,6 +7516,7 @@ export async function deleteWorkflow(id: string, source?: "api" | "mcp"): Promis
7610
7516
  );
7611
7517
  // 3. Delete runs (they reference workflow)
7612
7518
  await client.run("DELETE FROM workflow_runs WHERE workflowId = ?", [id]);
7519
+ await reclassifyTaskHumanFree(linkedTaskIds);
7613
7520
  // 4. Delete workflow
7614
7521
  const result = await client.run("DELETE FROM workflows WHERE id = ?", [id]);
7615
7522
  const deleted = result.changes > 0;
@@ -12112,6 +12019,21 @@ export async function deleteUser(id: string, replacementUserId?: string): Promis
12112
12019
  )`,
12113
12020
  );
12114
12021
  const replacement = replacementUserId ?? null;
12022
+ // The human-free rule only asks whether a task's requester or its workflow
12023
+ // run's creator is NULL, so only clearing them (no replacement) can change a
12024
+ // stored flag. Gather the tasks now: after the rewrite nothing identifies
12025
+ // them. The workflow branch covers roots whose requester was never set.
12026
+ const reclassifySeedIds: string[] = replacementUserId
12027
+ ? []
12028
+ : (
12029
+ await tx.query<{ id: string }>(
12030
+ `SELECT id FROM agent_tasks WHERE requestedByUserId = ?
12031
+ UNION
12032
+ SELECT id FROM agent_tasks
12033
+ WHERE workflowRunId IN (SELECT id FROM workflow_runs WHERE created_by = ?)`,
12034
+ [id, id],
12035
+ )
12036
+ ).map((row) => row.id);
12115
12037
  for (const reference of references) {
12116
12038
  const table = quoteSqlIdentifier(reference.tableName);
12117
12039
  const column = quoteSqlIdentifier(reference.columnName);
@@ -12139,6 +12061,8 @@ export async function deleteUser(id: string, replacementUserId?: string): Promis
12139
12061
  );
12140
12062
  }
12141
12063
 
12064
+ await reclassifyTaskHumanFree(reclassifySeedIds);
12065
+
12142
12066
  const result = await tx.run("DELETE FROM users WHERE id = ?", [id]);
12143
12067
  return result.changes > 0;
12144
12068
  });