@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.158.0",
3
+ "version": "1.159.0",
4
4
  "description": "agent-swarm.dev — an open-source operating system for AI work: a lead agent breaks goals into tasks, routes them to specialized workers such as Claude Code or Codex, runs each worker in an isolated container, and preserves shared memory, tools, schedules, and review gates so delegated work compounds across sessions.",
5
5
  "keywords": [
6
6
  "agent-swarm.dev",
@@ -169,8 +169,9 @@
169
169
  "devDependencies": {
170
170
  "@biomejs/biome": "^2.3.10",
171
171
  "@desplega.ai/slack-mock": "^0.4.0",
172
+ "@desplega/model-catalog": "workspace:*",
172
173
  "@faker-js/faker": "^10.4.0",
173
- "@opencode-ai/plugin": "1.17.4",
174
+ "@opencode-ai/plugin": "1.18.33",
174
175
  "@types/bun": "latest",
175
176
  "dependency-cruiser": "^18.0.0",
176
177
  "konsistent": "1.0.0-beta.1",
@@ -187,20 +188,20 @@
187
188
  "dependencies": {
188
189
  "@agentclientprotocol/sdk": "1.4.0",
189
190
  "@ai-sdk/openai": "^3.0.41",
190
- "@anthropic-ai/claude-agent-sdk": "0.3.266",
191
+ "@anthropic-ai/claude-agent-sdk": "0.3.285",
191
192
  "@anthropic-ai/sdk": "^0.93.0",
192
193
  "@asteasolutions/zod-to-openapi": "^8.0.0",
193
194
  "@aws-sdk/client-bedrock": "3.1048.0",
194
195
  "@desplega.ai/business-use": "^0.4.2",
195
196
  "@desplega.ai/localtunnel": "^2.2.0",
196
- "@earendil-works/pi-agent-core": "^0.87.1",
197
- "@earendil-works/pi-ai": "^0.87.1",
198
- "@earendil-works/pi-coding-agent": "^0.87.1",
197
+ "@earendil-works/pi-agent-core": "^0.99.1",
198
+ "@earendil-works/pi-ai": "^0.99.1",
199
+ "@earendil-works/pi-coding-agent": "^0.99.1",
199
200
  "@inkjs/ui": "^2.0.0",
200
201
  "@linear/sdk": "^77.0.0",
201
202
  "@modelcontextprotocol/sdk": "1.29.0",
202
- "@openai/codex": "0.157.1",
203
- "@opencode-ai/sdk": "^1.18.32",
203
+ "@openai/codex": "0.159.1",
204
+ "@opencode-ai/sdk": "^1.18.33",
204
205
  "@openfort/openfort-node": "^0.9.1",
205
206
  "@opentelemetry/api": "^1.9.1",
206
207
  "@opentelemetry/api-logs": "^0.218.0",
@@ -282,6 +282,26 @@ export async function getAllAgents(opts?: {
282
282
  return rows.map((row) => rowToAgent(row, opts?.slim ?? false));
283
283
  }
284
284
 
285
+ /** Counts non-extension agents by status without loading their profile fields. */
286
+ export async function getAgentStatusCounts(): Promise<{
287
+ total: number;
288
+ idle: number;
289
+ busy: number;
290
+ offline: number;
291
+ }> {
292
+ const rows = await getDbClient().query<{ status: AgentStatus; count: number }>(
293
+ `SELECT status, COUNT(*) AS count FROM agents WHERE ${NOT_EXTENSION_AGENT_SQL} GROUP BY status`,
294
+ );
295
+ const counts = { total: 0, idle: 0, busy: 0, offline: 0 };
296
+ for (const { status, count } of rows) {
297
+ counts.total += count;
298
+ if (status === "idle" || status === "busy" || status === "offline") {
299
+ counts[status] = count;
300
+ }
301
+ }
302
+ return counts;
303
+ }
304
+
285
305
  export async function getLeadAgent(): Promise<Agent | null> {
286
306
  const leads = (await getAllAgents()).filter((a) => a.isLead);
287
307
  // Prefer a usable (non-offline) lead so callers route to one that can actually
@@ -0,0 +1,442 @@
1
+ import { getDbClient } from "../runtime";
2
+
3
+ /**
4
+ * Structurally-human-free tasks: the swarm maintaining itself, with no human
5
+ * requester by construction. Heartbeat and boot-triage tasks, scheduled runs
6
+ * without a human creator (including workflow roots launched by such a
7
+ * schedule), and `source='system'` follow-ups whose parent itself has no human
8
+ * requester. The classification propagates to a child while the child stays
9
+ * unattributed (no requester, or one copied from the parent), so autonomous
10
+ * fan-out cannot leak back into the human denominator. An explicitly attributed
11
+ * child is an independent handoff and stops propagation down that branch.
12
+ *
13
+ * The result is stored on `agent_tasks.isHumanFree` when the task is created
14
+ * (migration 182 backfilled history), so the usage reports read a column
15
+ * instead of rebuilding a recursive CTE over every task per query.
16
+ *
17
+ * The stored flag must equal what the rule selects over the CURRENT rows, so a
18
+ * code path that rewrites a classifying input has to call
19
+ * `reclassifyTaskHumanFree` in the same transaction. The inputs are
20
+ * `agent_tasks.{taskType,tags,source,requestedByUserId,requestedByUserIdInherited,parentTaskId,workflowRunId}`,
21
+ * `workflow_runs.{triggerType,created_by}`, and the existence of the parent and
22
+ * run rows. Today these mutate in four places: `deleteUser` (requester and
23
+ * workflow creator cleared), `deleteWorkflow` (run link cleared, runs deleted),
24
+ * `deleteTask` (a parent row disappears) and `completeTask` with `addTags`,
25
+ * which only needs it when `tagWriteChangesHumanFree` says the tag write can
26
+ * change the outcome.
27
+ *
28
+ * Reclassification is bounded per request, because its cost follows the size of
29
+ * a task tree that a caller shapes: one batch runs inline and the rest of a
30
+ * large tree waits in `human_free_reclassify_queue` for a background drain
31
+ * (`src/be/human-free-drain.ts`). The invariant above holds once the queue is
32
+ * empty.
33
+ */
34
+
35
+ /**
36
+ * The tags the classification reads. The SQL below is generated from this list
37
+ * and `tagWriteChangesHumanFree` reads it too, so the guard on tag writes cannot
38
+ * drift from the classifier. Keep each entry to plain word characters: the SQL
39
+ * matches it with `LIKE`, where `%` and `_` are wildcards.
40
+ */
41
+ export const HUMAN_FREE_TAGS = ["heartbeat"] as const;
42
+
43
+ const HUMAN_FREE_TAG_SQL = HUMAN_FREE_TAGS.map(
44
+ (tag) => `COALESCE(task.tags, '[]') LIKE '%"${tag}"%'`,
45
+ ).join("\n OR ");
46
+
47
+ /**
48
+ * Non-propagated part of the classification, over the alias `task` (the row)
49
+ * and `parent` (a LEFT JOIN of `agent_tasks` on `task.parentTaskId`).
50
+ */
51
+ const HUMAN_FREE_BASE_SQL = `(
52
+ COALESCE(task.taskType, '') IN ('heartbeat', 'heartbeat-checklist', 'boot-triage')
53
+ OR ${HUMAN_FREE_TAG_SQL}
54
+ OR (COALESCE(task.source, '') = 'schedule' AND task.requestedByUserId IS NULL)
55
+ OR (
56
+ task.parentTaskId IS NULL
57
+ AND COALESCE(task.source, '') = 'workflow'
58
+ AND task.requestedByUserId IS NULL
59
+ AND EXISTS (
60
+ SELECT 1
61
+ FROM workflow_runs run
62
+ WHERE run.id = task.workflowRunId
63
+ AND run.triggerType = 'schedule'
64
+ AND run.created_by IS NULL
65
+ )
66
+ )
67
+ OR (
68
+ COALESCE(task.source, '') = 'system'
69
+ AND parent.id IS NOT NULL
70
+ AND parent.requestedByUserId IS NULL
71
+ )
72
+ )`;
73
+
74
+ /**
75
+ * Whether a task stays unattributed, so the parent's classification propagates
76
+ * to it: no requester, or one copied from the parent. Over the alias `task`.
77
+ */
78
+ const UNATTRIBUTED_SQL = "(task.requestedByUserId IS NULL OR task.requestedByUserIdInherited = 1)";
79
+
80
+ /**
81
+ * The classification as a 0/1 expression over the aliases `task` (the row) and
82
+ * `parent` (see above). `parentFlag` is the SQL expression for the parent's
83
+ * already-decided flag: the stored column for a task classified on its own, the
84
+ * freshly computed value while reclassifying a tree top-down.
85
+ */
86
+ function humanFreeCase(parentFlag: string): string {
87
+ return `CASE
88
+ WHEN ${HUMAN_FREE_BASE_SQL} THEN 1
89
+ WHEN ${parentFlag} = 1 AND ${UNATTRIBUTED_SQL} THEN 1
90
+ ELSE 0
91
+ END`;
92
+ }
93
+
94
+ function carriesHumanFreeTag(tagsJson: string | null | undefined): boolean {
95
+ // Mirror the SQL: `LIKE` matches the quoted tag inside the JSON text and is
96
+ // ASCII case-insensitive. Lowercasing is at least as broad, so this can only
97
+ // over-report a change, never miss one.
98
+ const haystack = (tagsJson ?? "[]").toLowerCase();
99
+ return HUMAN_FREE_TAGS.some((tag) => haystack.includes(`"${tag}"`));
100
+ }
101
+
102
+ /**
103
+ * Whether rewriting a task's `tags` column from `previousTagsJson` to
104
+ * `nextTagsJson` can change its classification. A tag write that adds or drops
105
+ * no tag from `HUMAN_FREE_TAGS` (a `deferred` tag, say) cannot, so the caller
106
+ * skips `reclassifyTaskHumanFree` and its walk over the task's whole subtree.
107
+ */
108
+ export function tagWriteChangesHumanFree(
109
+ previousTagsJson: string | null | undefined,
110
+ nextTagsJson: string | null | undefined,
111
+ ): boolean {
112
+ return carriesHumanFreeTag(previousTagsJson) !== carriesHumanFreeTag(nextTagsJson);
113
+ }
114
+
115
+ export interface HumanFreeInput {
116
+ taskType?: string | null;
117
+ /** The JSON-encoded `tags` column value. */
118
+ tags?: string | null;
119
+ source?: string | null;
120
+ requestedByUserId?: string | null;
121
+ requestedByUserIdInherited?: boolean;
122
+ parentTaskId?: string | null;
123
+ workflowRunId?: string | null;
124
+ }
125
+
126
+ /**
127
+ * Classify a task about to be inserted. Runs the same SQL predicate the
128
+ * migration 182 backfill used, so a task classified here and one classified by
129
+ * the backfill can never disagree. The parent must already be stored.
130
+ */
131
+ export async function classifyTaskHumanFree(input: HumanFreeInput): Promise<boolean> {
132
+ const row = await getDbClient().get<{ humanFree: number }>(
133
+ `WITH task(taskType, tags, source, requestedByUserId, requestedByUserIdInherited, parentTaskId, workflowRunId) AS (
134
+ VALUES (?, ?, ?, ?, ?, ?, ?)
135
+ )
136
+ SELECT ${humanFreeCase("parent.isHumanFree")} AS humanFree
137
+ FROM task
138
+ LEFT JOIN agent_tasks parent ON parent.id = task.parentTaskId`,
139
+ [
140
+ input.taskType ?? null,
141
+ input.tags ?? null,
142
+ input.source ?? null,
143
+ input.requestedByUserId ?? null,
144
+ input.requestedByUserIdInherited ? 1 : 0,
145
+ input.parentTaskId ?? null,
146
+ input.workflowRunId ?? null,
147
+ ],
148
+ );
149
+ return row?.humanFree === 1;
150
+ }
151
+
152
+ /**
153
+ * How many tasks one reclassification batch may touch. A request that changes a
154
+ * classifying input does at most this much work inline; whatever is left of the
155
+ * subtree is queued and drained in batches of the same size.
156
+ */
157
+ export const HUMAN_FREE_RECLASSIFY_BATCH = 500;
158
+
159
+ /**
160
+ * How many batches deep a queued continuation may go. A legitimate chain needs
161
+ * depth / batch size of them, so this is far past any real tree. It only stops
162
+ * a parent cycle longer than one batch (which nothing in the product writes)
163
+ * from re-queuing itself for ever.
164
+ */
165
+ export const HUMAN_FREE_MAX_CONTINUATION_HOPS = 10_000;
166
+
167
+ export interface ReclassifyOptions {
168
+ /** Tasks per batch. Defaults to `HUMAN_FREE_RECLASSIFY_BATCH`. */
169
+ batchSize?: number;
170
+ }
171
+
172
+ let queueListener: (() => void) | undefined;
173
+
174
+ /**
175
+ * The background drain registers here to hear about new queue rows, so it wakes
176
+ * on demand instead of polling. Pass `undefined` to detach.
177
+ */
178
+ export function setHumanFreeQueueListener(listener: (() => void) | undefined): void {
179
+ queueListener = listener;
180
+ }
181
+
182
+ async function enqueueHumanFreeWork(
183
+ taskIds: readonly string[],
184
+ scope: "self" | "children",
185
+ hops: number,
186
+ ): Promise<void> {
187
+ if (taskIds.length === 0) return;
188
+ if (hops > HUMAN_FREE_MAX_CONTINUATION_HOPS) {
189
+ console.error(
190
+ `[human-free] dropping ${taskIds.length} ${scope} continuation(s) past ${HUMAN_FREE_MAX_CONTINUATION_HOPS} hops; the task graph likely has a parent cycle`,
191
+ );
192
+ return;
193
+ }
194
+ // `WHERE true` keeps the parser from reading ON CONFLICT as a join clause.
195
+ // A repeat enqueue restarts the row: its subtree was reclassified again, so
196
+ // the pages already walked are stale.
197
+ await getDbClient().run(
198
+ `INSERT INTO human_free_reclassify_queue (taskId, scope, afterRowid, hops)
199
+ SELECT value, ?, 0, ? FROM json_each(?) WHERE true
200
+ ON CONFLICT (taskId, scope) DO UPDATE SET
201
+ afterRowid = 0,
202
+ hops = MAX(hops, excluded.hops)`,
203
+ [scope, hops, JSON.stringify(taskIds)],
204
+ );
205
+ // afterCommit: a rolled-back mutation queues nothing, so it must not wake the drain.
206
+ getDbClient().afterCommit(() => queueListener?.());
207
+ }
208
+
209
+ /**
210
+ * Reclassify `seedTaskIds` and their descendants, breadth first, until
211
+ * `batchSize` tasks are covered, and queue the rest. `seedTaskIds` must not be
212
+ * longer than `batchSize`.
213
+ *
214
+ * The rule is a reachability query: a task is free when it classifies on its
215
+ * own, or when it is unattributed and its parent is free. So the new flags come
216
+ * from a fixpoint over the batch, with no walk order to get wrong: `affected` is
217
+ * the batch, and `free` starts from the affected tasks that are free on their
218
+ * own or hang off an unaffected free parent (its stored flag is current) and
219
+ * spreads to unattributed children inside the batch. A parent inside `affected`
220
+ * is never read from the column, since that value may be exactly what is being
221
+ * fixed.
222
+ *
223
+ * The batch is a prefix of a breadth-first walk, so every parent of an affected
224
+ * task is affected too, except at the seeds. What a batch leaves behind are the
225
+ * children of its tasks that it did not reach. Those are queued as `children`
226
+ * rows keyed by the parent, so a very wide parent costs one row, not one per
227
+ * child.
228
+ */
229
+ async function reclassifyBatch(
230
+ seedTaskIds: readonly string[],
231
+ batchSize: number,
232
+ hops: number,
233
+ ): Promise<number> {
234
+ const client = getDbClient();
235
+
236
+ // The recursive walk over task ids alone, like the rule itself, so it still
237
+ // terminates on a parent cycle. LIMIT stops the recursion once the batch is full.
238
+ const affected = (
239
+ await client.query<{ id: string }>(
240
+ `WITH RECURSIVE affected(id) AS (
241
+ SELECT value FROM json_each(?)
242
+
243
+ UNION
244
+
245
+ SELECT task.id
246
+ FROM affected
247
+ JOIN agent_tasks task ON task.parentTaskId = affected.id
248
+ LIMIT ?
249
+ )
250
+ SELECT id FROM affected`,
251
+ [JSON.stringify(seedTaskIds), batchSize],
252
+ )
253
+ ).map((row) => row.id);
254
+
255
+ // The "WITH RECURSIVE affected" prefix is what the tests count subtree
256
+ // recomputations by. The unary plus in the recursive step keeps the planner
257
+ // from probing `task` by id once per batch member for every free row (a
258
+ // quadratic scan of the batch, ~100ms at 500 tasks); it walks the
259
+ // `parentTaskId` index instead, and the IN only filters what that finds.
260
+ const result = await client.run(
261
+ `WITH RECURSIVE affected(id) AS (
262
+ SELECT value FROM json_each(?)
263
+ ),
264
+ free(id) AS (
265
+ SELECT task.id
266
+ FROM agent_tasks task
267
+ LEFT JOIN agent_tasks parent ON parent.id = task.parentTaskId
268
+ WHERE task.id IN (SELECT id FROM affected)
269
+ AND (
270
+ ${HUMAN_FREE_BASE_SQL}
271
+ OR (
272
+ parent.isHumanFree = 1
273
+ AND parent.id NOT IN (SELECT id FROM affected)
274
+ AND ${UNATTRIBUTED_SQL}
275
+ )
276
+ )
277
+
278
+ UNION
279
+
280
+ SELECT task.id
281
+ FROM free
282
+ JOIN agent_tasks task ON task.parentTaskId = free.id
283
+ WHERE +task.id IN (SELECT id FROM affected)
284
+ AND ${UNATTRIBUTED_SQL}
285
+ )
286
+ UPDATE agent_tasks
287
+ SET isHumanFree = CASE WHEN id IN (SELECT id FROM free) THEN 1 ELSE 0 END
288
+ WHERE id IN (SELECT id FROM affected)
289
+ AND isHumanFree != CASE WHEN id IN (SELECT id FROM free) THEN 1 ELSE 0 END`,
290
+ [JSON.stringify(affected)],
291
+ );
292
+
293
+ // A short batch means the walk ran out of tree. A full one may have stopped
294
+ // early, so find the affected tasks with a child outside the batch. EXISTS
295
+ // stops at the first such child, so the cost tracks the batch, not the fan-out.
296
+ if (affected.length >= batchSize) {
297
+ const unfinished = await client.query<{ id: string }>(
298
+ `WITH batch(id) AS (
299
+ SELECT value FROM json_each(?)
300
+ )
301
+ SELECT batch.id
302
+ FROM batch
303
+ WHERE EXISTS (
304
+ SELECT 1
305
+ FROM agent_tasks child
306
+ WHERE child.parentTaskId = batch.id
307
+ AND child.id NOT IN (SELECT id FROM batch)
308
+ )`,
309
+ [JSON.stringify(affected)],
310
+ );
311
+ await enqueueHumanFreeWork(
312
+ unfinished.map((row) => row.id),
313
+ "children",
314
+ hops,
315
+ );
316
+ }
317
+ return result.changes;
318
+ }
319
+
320
+ /**
321
+ * Recompute the stored flag for `seedTaskIds` and every descendant, after a
322
+ * mutation changed a classifying input. Call it inside the mutating
323
+ * transaction, once the new state is written, with ids gathered BEFORE the
324
+ * write when the write erases what identifies them (a cleared requester).
325
+ *
326
+ * The work per call is bounded. One batch of `HUMAN_FREE_RECLASSIFY_BATCH`
327
+ * tasks runs inline; a subtree or seed list larger than that leaves the rest in
328
+ * `human_free_reclassify_queue`, written in the same transaction, for
329
+ * `drainHumanFreeReclassifyQueue` to finish. Until it does, the stored flags of
330
+ * the queued descendants are the pre-mutation ones, so the usage reports can
331
+ * lag for a very large tree. They converge to what the rule selects over the
332
+ * current rows once the queue is empty.
333
+ *
334
+ * Returns how many stored flags this call's inline batch changed.
335
+ */
336
+ export async function reclassifyTaskHumanFree(
337
+ seedTaskIds: readonly string[],
338
+ options: ReclassifyOptions = {},
339
+ ): Promise<number> {
340
+ const batchSize = options.batchSize ?? HUMAN_FREE_RECLASSIFY_BATCH;
341
+ const seeds = [...new Set(seedTaskIds)];
342
+ if (seeds.length === 0) return 0;
343
+ await enqueueHumanFreeWork(seeds.slice(batchSize), "self", 1);
344
+ return await reclassifyBatch(seeds.slice(0, batchSize), batchSize, 1);
345
+ }
346
+
347
+ interface QueueRow {
348
+ seq: number;
349
+ taskId: string;
350
+ scope: "self" | "children";
351
+ afterRowid: number;
352
+ hops: number;
353
+ }
354
+
355
+ /** One batch of queued work, in its own transaction. Null when the queue is empty. */
356
+ async function drainOneBatch(batchSize: number): Promise<number | null> {
357
+ return await getDbClient().transaction(async (tx) => {
358
+ const head = await tx.get<QueueRow>(
359
+ "SELECT * FROM human_free_reclassify_queue ORDER BY seq LIMIT 1",
360
+ );
361
+ if (!head) return null;
362
+
363
+ if (head.scope === "self") {
364
+ const rows = await tx.query<QueueRow>(
365
+ "SELECT * FROM human_free_reclassify_queue WHERE scope = 'self' ORDER BY seq LIMIT ?",
366
+ [batchSize],
367
+ );
368
+ const seeds = rows.map((row) => row.taskId);
369
+ const hops = Math.max(...rows.map((row) => row.hops)) + 1;
370
+ await tx.run(
371
+ "DELETE FROM human_free_reclassify_queue WHERE seq IN (SELECT value FROM json_each(?))",
372
+ [JSON.stringify(rows.map((row) => row.seq))],
373
+ );
374
+ return await reclassifyBatch(seeds, batchSize, hops);
375
+ }
376
+
377
+ // Children are paged by rowid: the parentTaskId index is ordered by it, so
378
+ // each page is an index range scan however wide the parent is.
379
+ const page = await tx.query<{ id: string; position: number }>(
380
+ `SELECT id, rowid AS position
381
+ FROM agent_tasks
382
+ WHERE parentTaskId = ? AND rowid > ?
383
+ ORDER BY rowid
384
+ LIMIT ?`,
385
+ [head.taskId, head.afterRowid, batchSize],
386
+ );
387
+ // Move the cursor before reclassifying: the batch may queue this very row
388
+ // again (a parent cycle), and that restart must win.
389
+ if (page.length < batchSize) {
390
+ await tx.run("DELETE FROM human_free_reclassify_queue WHERE seq = ?", [head.seq]);
391
+ } else {
392
+ await tx.run("UPDATE human_free_reclassify_queue SET afterRowid = ? WHERE seq = ?", [
393
+ page[page.length - 1]?.position ?? head.afterRowid,
394
+ head.seq,
395
+ ]);
396
+ }
397
+ if (page.length === 0) return 0;
398
+ return await reclassifyBatch(
399
+ page.map((row) => row.id),
400
+ batchSize,
401
+ head.hops + 1,
402
+ );
403
+ });
404
+ }
405
+
406
+ export interface DrainResult {
407
+ /** Batches run. */
408
+ batches: number;
409
+ /** Stored flags changed across those batches. */
410
+ changed: number;
411
+ /** Queue rows left. */
412
+ remaining: number;
413
+ }
414
+
415
+ export async function pendingHumanFreeReclassifications(): Promise<number> {
416
+ const row = await getDbClient().get<{ count: number }>(
417
+ "SELECT COUNT(*) AS count FROM human_free_reclassify_queue",
418
+ );
419
+ return row?.count ?? 0;
420
+ }
421
+
422
+ /**
423
+ * Work the reclassification queue: up to `maxBatches` batches (default: until
424
+ * the queue is empty), each in its own transaction so the write lock is held
425
+ * for one bounded batch at a time. Anything a batch leaves behind goes back on
426
+ * the queue, so the queue empties only once every task matches the rule.
427
+ */
428
+ export async function drainHumanFreeReclassifyQueue(
429
+ options: ReclassifyOptions & { maxBatches?: number } = {},
430
+ ): Promise<DrainResult> {
431
+ const batchSize = options.batchSize ?? HUMAN_FREE_RECLASSIFY_BATCH;
432
+ const maxBatches = options.maxBatches ?? Number.POSITIVE_INFINITY;
433
+ let batches = 0;
434
+ let changed = 0;
435
+ while (batches < maxBatches) {
436
+ const batchChanged = await drainOneBatch(batchSize);
437
+ if (batchChanged === null) break;
438
+ batches += 1;
439
+ changed += batchChanged;
440
+ }
441
+ return { batches, changed, remaining: await pendingHumanFreeReclassifications() };
442
+ }
@@ -86,6 +86,9 @@ export type AgentTaskRow = {
86
86
  model: string | null;
87
87
  modelTier: string | null;
88
88
  effort: string | null;
89
+ resolvedModel?: string | null;
90
+ modelSource?: string | null;
91
+ modelAlias?: string | null;
89
92
  scheduleId: string | null;
90
93
  workflowRunId: string | null;
91
94
  workflowRunStepId: string | null;
@@ -146,6 +149,9 @@ type AgentTaskSummaryRow = Pick<
146
149
  | "scheduleId"
147
150
  | "model"
148
151
  | "modelTier"
152
+ | "resolvedModel"
153
+ | "modelSource"
154
+ | "modelAlias"
149
155
  | "effort"
150
156
  | "provider"
151
157
  | "requestedByUserId"
@@ -198,7 +204,8 @@ const LIST_COLUMNS = {
198
204
  ${TASK_PREVIEW_SQL}, agent_tasks.title, agent_tasks.status, agent_tasks.source,
199
205
  agent_tasks.taskType, agent_tasks.tags, agent_tasks.priority, agent_tasks.dependsOn,
200
206
  agent_tasks.offeredTo, agent_tasks.acceptedAt, agent_tasks.parentTaskId,
201
- agent_tasks.scheduleId, agent_tasks.model, agent_tasks.modelTier, agent_tasks.effort,
207
+ agent_tasks.scheduleId, agent_tasks.model, agent_tasks.modelTier,
208
+ agent_tasks.resolvedModel, agent_tasks.modelSource, agent_tasks.modelAlias, agent_tasks.effort,
202
209
  agent_tasks.provider, agent_tasks.requestedByUserId, agent_tasks.progress,
203
210
  agent_tasks.createdAt, agent_tasks.lastUpdatedAt, agent_tasks.finishedAt,
204
211
  agent_tasks.peakContextPercent`,
@@ -300,6 +307,9 @@ export function rowToAgentTask(row: AgentTaskRow): AgentTask {
300
307
  claudeSessionId: row.claudeSessionId ?? undefined,
301
308
  model: row.model ?? undefined,
302
309
  modelTier: parseModelTier(row.modelTier) ?? undefined,
310
+ resolvedModel: row.resolvedModel ?? undefined,
311
+ modelSource: row.modelSource ?? undefined,
312
+ modelAlias: row.modelAlias ?? undefined,
303
313
  effort: ReasoningEffortSchema.safeParse(row.effort).success
304
314
  ? (row.effort as ReasoningEffort)
305
315
  : undefined,
@@ -363,6 +373,9 @@ export function rowToAgentTaskSummary(row: AgentTaskSummaryRow): AgentTaskSummar
363
373
  scheduleId: row.scheduleId ?? undefined,
364
374
  model: row.model ?? undefined,
365
375
  modelTier: parseModelTier(row.modelTier) ?? undefined,
376
+ resolvedModel: row.resolvedModel ?? undefined,
377
+ modelSource: row.modelSource ?? undefined,
378
+ modelAlias: row.modelAlias ?? undefined,
366
379
  effort: ReasoningEffortSchema.safeParse(row.effort).success
367
380
  ? (row.effort as ReasoningEffort)
368
381
  : undefined,
@@ -458,6 +471,48 @@ export async function getSupersededTasksWithoutResume(
458
471
  return rows.map(rowToAgentTask);
459
472
  }
460
473
 
474
+ /** Statuses a dependent can hold before it ever started running. */
475
+ export const NEVER_STARTED_TASK_STATUSES = [
476
+ "draft",
477
+ "backlog",
478
+ "unassigned",
479
+ "offered",
480
+ "reviewing",
481
+ "pending",
482
+ ];
483
+
484
+ /**
485
+ * Superseded tasks that already have a resume child but still have
486
+ * never-started dependents waiting on them, paired with their latest resume.
487
+ * A crash after the resume is created but before
488
+ * `backfillSupersedeTaskResumeTaskId` leaves this state, and
489
+ * {@link getSupersededTasksWithoutResume} no longer sees it.
490
+ */
491
+ export async function getSupersededTasksWithUnsettledDependents(
492
+ finishedBefore: string,
493
+ finishedAfter: string,
494
+ ): Promise<{ taskId: string; resumeTaskId: string }[]> {
495
+ const placeholders = NEVER_STARTED_TASK_STATUSES.map(() => "?").join(", ");
496
+ return getDbClient().query<{ taskId: string; resumeTaskId: string }>(
497
+ `SELECT taskId, resumeTaskId FROM (
498
+ SELECT t.id AS taskId, t.finishedAt AS finishedAt,
499
+ (SELECT c.id FROM agent_tasks c
500
+ WHERE c.parentTaskId = t.id AND c.taskType = 'resume'
501
+ ORDER BY c.createdAt DESC LIMIT 1) AS resumeTaskId
502
+ FROM agent_tasks t
503
+ WHERE t.status = 'superseded'
504
+ AND t.finishedAt < ?
505
+ AND t.finishedAt > ?
506
+ AND EXISTS (
507
+ SELECT 1 FROM agent_tasks d, json_each(d.dependsOn) AS j
508
+ WHERE j.value = t.id AND d.status IN (${placeholders})
509
+ ))
510
+ WHERE resumeTaskId IS NOT NULL
511
+ ORDER BY finishedAt ASC`,
512
+ [finishedBefore, finishedAfter, ...NEVER_STARTED_TASK_STATUSES],
513
+ );
514
+ }
515
+
461
516
  /**
462
517
  * True when a non-terminal `reroute-decision` child exists for `parentId`.
463
518
  *