@desplega.ai/agent-swarm 1.158.0 → 1.160.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 (378) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-adapter-xaq4n6pc.js → acp-adapter-2mjwcsfh.js} +124 -27
  3. package/dist/{actions-p9hz5yat.js → actions-e43rxskr.js} +35 -30
  4. package/dist/{anthropic-messages-51pz26qe.js → anthropic-messages-5qx80j9k.js} +106 -16
  5. package/dist/{app-g26wdcz6.js → app-mc7xfagn.js} +12 -10
  6. package/dist/{assistant-wydfqkb3.js → assistant-sedkzv3b.js} +42 -31
  7. package/dist/{azure-openai-responses-8gksqtmz.js → azure-openai-responses-nfb21z9a.js} +13 -14
  8. package/dist/{boot-reembed-se8zmdcp.js → boot-reembed-cqz9swn5.js} +13 -12
  9. package/dist/{boot-reembed-g5ckg9yb.js → boot-reembed-m3656j2n.js} +14 -13
  10. package/dist/{boot-scrub-logs-wfxqan61.js → boot-scrub-logs-xbxf5d7w.js} +11 -9
  11. package/dist/boot-seeds-gsetbs5y.js +28 -0
  12. package/dist/{claude-adapter-tv2x4f36.js → claude-adapter-dc6289mc.js} +28 -23
  13. package/dist/{claude-managed-adapter-41b2s7fd.js → claude-managed-adapter-naaw51me.js} +4 -3
  14. package/dist/{claude-managed-setup-60g8aazx.js → claude-managed-setup-eacvchx0.js} +12 -3
  15. package/dist/claude-sdk-session-bqp2xvym.js +29722 -0
  16. package/dist/{cli-987hg0aa.js → cli-0jwe9034.js} +2 -2
  17. package/dist/{cli-vj6h9kc2.js → cli-0m7wct0d.js} +59 -1
  18. package/dist/{cli-c07r9a2m.js → cli-0q3avq3s.js} +12 -11
  19. package/dist/{cli-19jenss3.js → cli-0wgxmwf6.js} +31 -10
  20. package/dist/cli-0x01kwrd.js +491 -0
  21. package/dist/cli-0xqde53b.js +176 -0
  22. package/dist/cli-10nhj6xq.js +8355 -0
  23. package/dist/{cli-tt9k8r35.js → cli-12tszpk9.js} +479 -910
  24. package/dist/{cli-5b6b62tz.js → cli-13hw4z91.js} +1 -1
  25. package/dist/{cli-cqt19wg6.js → cli-1a2j50m0.js} +11 -2
  26. package/dist/{cli-xberxa1r.js → cli-25fcq4b4.js} +2 -2
  27. package/dist/{cli-npssxgaz.js → cli-2pdaysab.js} +52 -2
  28. package/dist/cli-3qhyh31j.js +166 -0
  29. package/dist/cli-447dt1h3.js +3828 -0
  30. package/dist/cli-4derpgyk.js +115 -0
  31. package/dist/{cli-00w2c66p.js → cli-523679eh.js} +13 -2
  32. package/dist/{cli-y95wp3tg.js → cli-58e8yr0d.js} +1 -1
  33. package/dist/{cli-24k806sc.js → cli-5nbmpy8w.js} +3 -3
  34. package/dist/{cli-53sz8gkz.js → cli-6cs6wzxt.js} +1 -1
  35. package/dist/cli-741yb0xx.js +1027 -0
  36. package/dist/cli-74h9hscv.js +69 -0
  37. package/dist/cli-7cxw8b60.js +14910 -0
  38. package/dist/{cli-xhaeg2nc.js → cli-7e398j1h.js} +1 -1
  39. package/dist/cli-7kyk9jr3.js +22 -0
  40. package/dist/cli-89s4ngw3.js +183 -0
  41. package/dist/{cli-9b13ggd1.js → cli-89v6kyq7.js} +1 -0
  42. package/dist/{cli-ftfvggzx.js → cli-9hsgwz4w.js} +1045 -581
  43. package/dist/{cli-rqz8qbw7.js → cli-a8dvhzrt.js} +59 -20
  44. package/dist/{cli-j57qkrpq.js → cli-azhgzetg.js} +395 -56
  45. package/dist/cli-br0qdrnp.js +2150 -0
  46. package/dist/{cli-rj19hc34.js → cli-btfv8y88.js} +1 -1
  47. package/dist/{cli-8qmme6vd.js → cli-c8a36vcc.js} +2 -2
  48. package/dist/cli-cpvsaqsw.js +333 -0
  49. package/dist/cli-d5czme6m.js +17805 -0
  50. package/dist/{cli-h38x0qhg.js → cli-et8m9xsv.js} +4 -3
  51. package/dist/{cli-x24pshtq.js → cli-evvtk35y.js} +6 -6
  52. package/dist/{cli-7s3syk22.js → cli-f8gy6r2r.js} +5 -5
  53. package/dist/{cli-zt1227k1.js → cli-fkqhbg8g.js} +5 -3
  54. package/dist/{cli-9httdpyq.js → cli-g2whhdrf.js} +1 -1
  55. package/dist/cli-gt4hxb8p.js +99 -0
  56. package/dist/{cli-s8eet254.js → cli-jajnsm8q.js} +37184 -53838
  57. package/dist/{cli-7gtvvpbh.js → cli-jayjpjmb.js} +21 -12
  58. package/dist/{cli-68w09sjw.js → cli-jyk0ynw2.js} +27 -89
  59. package/dist/{cli-aehm1rbq.js → cli-jz4w45qt.js} +1152 -61
  60. package/dist/cli-kmcpfwn2.js +333856 -0
  61. package/dist/{cli-4198qc9v.js → cli-ktmyrtpn.js} +19 -3
  62. package/dist/{cli-j55n64b6.js → cli-mfe5btp9.js} +505 -189
  63. package/dist/{cli-q67y4wzm.js → cli-phsaj4xq.js} +24 -2
  64. package/dist/{cli-1hkrch3k.js → cli-pm1rst0h.js} +46 -7
  65. package/dist/{cli-pw9rj5t9.js → cli-pqetb83f.js} +1 -176
  66. package/dist/{cli-ggm92wcn.js → cli-ptr8h7kp.js} +20 -1
  67. package/dist/{cli-s8s499xy.js → cli-qbksc292.js} +3 -2
  68. package/dist/{cli-47m6dkd3.js → cli-qydf22qq.js} +597 -346
  69. package/dist/{cli-3gmcwfyx.js → cli-r1mxd1xh.js} +13 -6
  70. package/dist/cli-rb9m6rpe.js +4980 -0
  71. package/dist/cli-rykfmgwg.js +102 -0
  72. package/dist/{cli-5d10vd2m.js → cli-shkxm1vn.js} +6 -5
  73. package/dist/{cli-eetrvv91.js → cli-t3p77qk1.js} +1 -1
  74. package/dist/cli-tfky2838.js +756 -0
  75. package/dist/{cli-g8djp6qe.js → cli-tmqt7dpj.js} +40 -0
  76. package/dist/{cli-fgqxwrpv.js → cli-tpq83z89.js} +1631 -127
  77. package/dist/cli-trc24wk2.js +143 -0
  78. package/dist/{cli-aky8tctm.js → cli-tzx2ke1v.js} +3 -3
  79. package/dist/cli-v7fn5gms.js +1425 -0
  80. package/dist/{cli-wscdvcqz.js → cli-vctdqs5h.js} +1 -1
  81. package/dist/{cli-cw9zck8j.js → cli-xjcywfsf.js} +8 -4
  82. package/dist/{cli-k0g1sjzm.js → cli-ybgv4kw2.js} +18 -11
  83. package/dist/{cli-rjehvrts.js → cli-ydmcgve1.js} +8 -8
  84. package/dist/{cli-vhjwh6cz.js → cli-ygwm91nt.js} +1 -1
  85. package/dist/{cli-mcnhgnn1.js → cli-ykk5jaax.js} +184 -26
  86. package/dist/{cli-de1396j2.js → cli-ywybs5as.js} +7 -1
  87. package/dist/cli-zpep9q5e.js +491 -0
  88. package/dist/cli.js +41 -32
  89. package/dist/cloudflare-workers-ai-system-one-k6fbxw9d.js +46 -0
  90. package/dist/{codex-adapter-cnm8wk11.js → codex-adapter-nmxcatww.js} +26 -17
  91. package/dist/{codex-hook-9hptmdhj.js → codex-hook-20me6byw.js} +2 -2
  92. package/dist/{codex-login-x3jk4etn.js → codex-login-w6fz4xbz.js} +50 -5
  93. package/dist/{codex-session-runner-gfg547zj.js → codex-session-runner-90aa80ev.js} +24 -17
  94. package/dist/{commands-e3bn0p75.js → commands-8wcq1kn7.js} +11 -9
  95. package/dist/{db-03v9kzr2.js → db-j40kc4se.js} +25 -11
  96. package/dist/{devin-adapter-8vgkwh29.js → devin-adapter-dpsv6hd9.js} +1 -1
  97. package/dist/{dispatcher-nrmg8yht.js → dispatcher-4zqy768d.js} +34 -29
  98. package/dist/{dsh-adapter-k03nfqa6.js → dsh-adapter-mpnzgztj.js} +3 -3
  99. package/dist/{e2b-93x1k0j5.js → e2b-ez07znam.js} +4 -3
  100. package/dist/execute-6e7qeyk4.js +1017 -0
  101. package/dist/{google-generative-ai-bxmv50nc.js → google-generative-ai-k6n5nbpp.js} +6 -5
  102. package/dist/{google-vertex-809af514.js → google-vertex-44xxtcvp.js} +6 -5
  103. package/dist/{handlers-aqcedbyb.js → handlers-kjdce1w5.js} +37 -32
  104. package/dist/{hook-ybvmrmhq.js → hook-czx13n12.js} +19 -15
  105. package/dist/{hook-7vkskkre.js → hook-r2t4bqfg.js} +18 -14
  106. package/dist/{http-2eqa8t24.js → http-3bbb19be.js} +900 -145
  107. package/dist/{index-f6cryxwe.js → index-4a5njnm1.js} +2 -59
  108. package/dist/{index-x71bvq0p.js → index-668me3f0.js} +35 -30
  109. package/dist/{index-c6zsv1bd.js → index-7aj368tw.js} +13 -12
  110. package/dist/{index-1yks3qcr.js → index-fg0dsp6c.js} +34 -29
  111. package/dist/{index-chgdkq36.js → index-kkv84jq9.js} +34 -29
  112. package/dist/{index-a3vn96n8.js → index-zg7vp46x.js} +198 -54
  113. package/dist/{keepalive-vtkf5n99.js → keepalive-yra8dy57.js} +13 -11
  114. package/dist/{lead-h3sp357x.js → lead-va0pad2c.js} +54 -49
  115. package/dist/llama-cpp-classify-tgj35k8d.js +335 -0
  116. package/dist/{maintenance-jzeqtk3q.js → maintenance-hqewtmyg.js} +14 -13
  117. package/dist/{mistral-conversations-zx0kn554.js → mistral-conversations-pc3ntnxt.js} +14 -17
  118. package/dist/{oauth-refresh-sweep-qp3dzmdp.js → oauth-refresh-sweep-7rdt1m9n.js} +12 -10
  119. package/dist/{onboard-tmsp0qbz.js → onboard-j6ms6d0x.js} +6 -5
  120. package/dist/{openai-codex-responses-4vjqzvjp.js → openai-codex-responses-bbancrr0.js} +21 -8
  121. package/dist/{openai-completions-9hjrqbw8.js → openai-completions-jpaj5g2b.js} +9 -12
  122. package/dist/{openai-responses-7b1pn0xt.js → openai-responses-200k1wx4.js} +23 -18
  123. package/dist/{opencode-adapter-x6x2da1z.js → opencode-adapter-89rabkw2.js} +8 -7
  124. package/dist/{openrouter-images-9f9ek4rw.js → openrouter-images-8yx3487g.js} +5 -7
  125. package/dist/{otel-impl-9mz73q65.js → otel-impl-a792rjz0.js} +1 -1
  126. package/dist/{pi-messages-txpjj39q.js → pi-messages-vxdt8v7j.js} +2 -1
  127. package/dist/{pi-mono-adapter-m6dvv0dz.js → pi-mono-adapter-g7qzvr3v.js} +295 -70
  128. package/dist/pricing-refresh-wx0tp7cb.js +39 -0
  129. package/dist/quickjs-r1pq2107.wasm +0 -0
  130. package/dist/{rbac-roles-c9wqz83x.js → rbac-roles-8tzawacv.js} +12 -10
  131. package/dist/{render-v2-gryrh9h8.js → render-v2-mm7zswb8.js} +12 -10
  132. package/dist/runtime-zyssgse0.js +24 -0
  133. package/dist/{setup-q9720bt6.js → setup-8y70ys6t.js} +6 -5
  134. package/dist/typesafe-system-one-2ef30pec.js +27 -0
  135. package/dist/{virtual-modules-3htpgn8x.js → virtual-modules-6nmk7h3n.js} +22 -13
  136. package/dist/{work-objects-b76x7deb.js → work-objects-fg6xkb0n.js} +37 -32
  137. package/dist/{worker-6vrxbv41.js → worker-wmwhxsph.js} +54 -49
  138. package/openapi.json +1314 -91
  139. package/package.json +12 -11
  140. package/src/be/db/agents.ts +20 -0
  141. package/src/be/db/api-keys.ts +70 -0
  142. package/src/be/db/tasks/human-free.ts +442 -0
  143. package/src/be/db/tasks/read.ts +56 -1
  144. package/src/be/db/tasks/write.ts +107 -11
  145. package/src/be/db.ts +274 -188
  146. package/src/be/harness-model-support.ts +182 -0
  147. package/src/be/human-free-drain.ts +67 -0
  148. package/src/be/memory/providers/sqlite-store.ts +51 -15
  149. package/src/be/migrations/182_task_human_free_flag.sql +63 -0
  150. package/src/be/migrations/183_human_free_reclassify_queue.sql +25 -0
  151. package/src/be/migrations/184_model_catalog.sql +120 -0
  152. package/src/be/migrations/185_api_key_auth_failures.sql +6 -0
  153. package/src/be/migrations/186_api_key_seat_mismatch.sql +8 -0
  154. package/src/be/model-catalog-store.ts +644 -0
  155. package/src/be/model-tier-keys.ts +46 -0
  156. package/src/be/model-tier-resolution.ts +534 -0
  157. package/src/be/model-validation.ts +188 -0
  158. package/src/be/models-catalog.ts +61 -7
  159. package/src/be/modelsdev-cache.json +82 -0
  160. package/src/be/modelsdev-cache.ts +2 -0
  161. package/src/be/pricing-refresh.ts +154 -12
  162. package/src/be/scripts/typecheck.ts +15 -0
  163. package/src/be/seed/runner.ts +18 -12
  164. package/src/be/seed/types.ts +1 -0
  165. package/src/be/seed-scripts/catalog/task-failure-audit.ts +61 -9
  166. package/src/be/swarm-config-guard.ts +8 -0
  167. package/src/cli.tsx +10 -0
  168. package/src/commands/claude-managed-setup.ts +3 -1
  169. package/src/commands/codex-login.ts +67 -0
  170. package/src/commands/credential-outcome-report.ts +191 -0
  171. package/src/commands/rate-limit-outcome.ts +34 -4
  172. package/src/commands/runner.ts +357 -186
  173. package/src/commands/seat-mismatch-report.ts +46 -0
  174. package/src/extensions/apply-task-create.ts +27 -1
  175. package/src/extensions/contract-types.generated.ts +1 -1
  176. package/src/heartbeat/heartbeat.ts +79 -17
  177. package/src/heartbeat/templates.ts +2 -2
  178. package/src/http/agents.ts +62 -1
  179. package/src/http/api-keys.ts +140 -2
  180. package/src/http/codex-oauth-keep-warm.ts +3 -5
  181. package/src/http/index.ts +8 -0
  182. package/src/http/memory.ts +12 -5
  183. package/src/http/models-catalog.ts +357 -8
  184. package/src/http/poll.ts +54 -3
  185. package/src/http/schedules.ts +51 -6
  186. package/src/http/script-runs.ts +12 -0
  187. package/src/http/session-data.ts +2 -5
  188. package/src/http/stats.ts +6 -8
  189. package/src/http/status.ts +15 -1
  190. package/src/http/tasks.ts +90 -7
  191. package/src/http/usage-cache.ts +56 -15
  192. package/src/http/workflows.ts +46 -8
  193. package/src/mcp-client/http-client.ts +5 -0
  194. package/src/memory/recall-query.ts +74 -0
  195. package/src/prompts/base-prompt.ts +15 -4
  196. package/src/prompts/session-templates.ts +23 -1
  197. package/src/providers/acp-adapter.ts +1 -0
  198. package/src/providers/claude-adapter.ts +14 -5
  199. package/src/providers/claude-managed-adapter.ts +1 -0
  200. package/src/providers/claude-managed-models.ts +47 -161
  201. package/src/providers/claude-sdk-session.ts +3 -1
  202. package/src/providers/codex-adapter.ts +14 -14
  203. package/src/providers/codex-models.ts +65 -198
  204. package/src/providers/devin-adapter.ts +1 -0
  205. package/src/providers/dsh-adapter.ts +2 -1
  206. package/src/providers/harness-version.ts +83 -7
  207. package/src/providers/opencode-adapter.ts +53 -1
  208. package/src/providers/pi-binary-root/src/extensions/codemode/worker.ts +11 -0
  209. package/src/providers/pi-codemode-runtime.ts +17 -0
  210. package/src/providers/pi-mono-adapter.ts +361 -83
  211. package/src/providers/pi-mono-extension.ts +16 -1
  212. package/src/providers/reasoning-effort.ts +44 -140
  213. package/src/providers/types.ts +8 -0
  214. package/src/rbac/legacy-policy.ts +43 -0
  215. package/src/rbac/permissions.ts +15 -0
  216. package/src/scripts-runtime/sdk-allowlist.ts +4 -0
  217. package/src/scripts-runtime/types/stdlib.d.ts +15 -0
  218. package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
  219. package/src/scripts-runtime/types/swarm-sdk.d.ts +15 -0
  220. package/src/server.ts +6 -0
  221. package/src/slack/assistant.ts +13 -0
  222. package/src/slack/message-text.ts +56 -2
  223. package/src/slack/render-v2.ts +87 -37
  224. package/src/slack/session-status.ts +541 -0
  225. package/src/slack/thread-buffer.ts +10 -2
  226. package/src/tasks/sibling-awareness.ts +3 -2
  227. package/src/telemetry.ts +5 -4
  228. package/src/templates.d.ts +6 -0
  229. package/src/tests/agents-list-model-display.test.ts +4 -6
  230. package/src/tests/api-key-auth-failure-route.test.ts +142 -0
  231. package/src/tests/api-key-tracking.test.ts +368 -1
  232. package/src/tests/base-prompt.test.ts +43 -2
  233. package/src/tests/claude-adapter.test.ts +68 -0
  234. package/src/tests/claude-managed-adapter.test.ts +12 -18
  235. package/src/tests/codex-adapter.test.ts +2 -1
  236. package/src/tests/codex-auth-failure.test.ts +52 -0
  237. package/src/tests/codex-gpt6-release.test.ts +66 -5
  238. package/src/tests/codex-login.test.ts +116 -0
  239. package/src/tests/context-window.test.ts +7 -5
  240. package/src/tests/credential-exhaustion-policy.test.ts +118 -3
  241. package/src/tests/credential-outcome-report.test.ts +171 -0
  242. package/src/tests/credential-status-api.test.ts +103 -2
  243. package/src/tests/db-extraction-contract.test.ts +1 -0
  244. package/src/tests/db-task-read-contract.test.ts +10 -1
  245. package/src/tests/db-task-write-contract.test.ts +535 -2
  246. package/src/tests/docs-index-footprint.test.js +3 -3
  247. package/src/tests/dokploy-deploy.test.ts +315 -0
  248. package/src/tests/dsh-adapter.test.ts +1 -1
  249. package/src/tests/explicit-model-validation.test.ts +583 -0
  250. package/src/tests/extensions-pre-task.test.ts +73 -0
  251. package/src/tests/fetch-resolved-env.test.ts +46 -0
  252. package/src/tests/hang-watchdog-worker.ts +47 -0
  253. package/src/tests/hang-watchdog.test.ts +45 -0
  254. package/src/tests/hang-watchdog.ts +30 -0
  255. package/src/tests/harness-model-guard.test.ts +123 -0
  256. package/src/tests/harness-model-support.test.ts +374 -0
  257. package/src/tests/harness-version.test.ts +70 -3
  258. package/src/tests/heartbeat-checklist-config.test.ts +71 -0
  259. package/src/tests/heartbeat-checklist.test.ts +4 -2
  260. package/src/tests/heartbeat-orphan-resume.test.ts +48 -0
  261. package/src/tests/http-task-write-ownership.test.ts +172 -0
  262. package/src/tests/human-free-flag.test.ts +279 -0
  263. package/src/tests/human-free-oracle.ts +57 -0
  264. package/src/tests/human-free-reclassify.test.ts +669 -0
  265. package/src/tests/memory-fts-populate.test.ts +139 -0
  266. package/src/tests/memory-http-recall-gating.test.ts +125 -1
  267. package/src/tests/migration-046-budgets.test.ts +3 -3
  268. package/src/tests/model-catalog-migration.test.ts +134 -0
  269. package/src/tests/model-catalog-table.test.ts +249 -0
  270. package/src/tests/model-catalog-write-access.test.ts +387 -0
  271. package/src/tests/model-rate-limit-windows.test.ts +19 -0
  272. package/src/tests/model-tier-resolution.test.ts +611 -0
  273. package/src/tests/models-catalog.test.ts +1 -1
  274. package/src/tests/multi-runtime-enabled-stats.test.ts +65 -1
  275. package/src/tests/opencode-adapter.test.ts +157 -2
  276. package/src/tests/package-publish.test.ts +137 -36
  277. package/src/tests/pi-codemode.test.ts +246 -0
  278. package/src/tests/pi-mono-adapter.test.ts +3 -0
  279. package/src/tests/pi-native-mcp.test.ts +193 -0
  280. package/src/tests/pi-tool-exposure.test.ts +374 -0
  281. package/src/tests/preload.ts +3 -0
  282. package/src/tests/pricing-routes.test.ts +2 -2
  283. package/src/tests/prompt-template-session.test.ts +5 -3
  284. package/src/tests/rate-limit-event.test.ts +65 -0
  285. package/src/tests/rate-limit-outcome.test.ts +90 -0
  286. package/src/tests/rbac-engine.test.ts +77 -3
  287. package/src/tests/reasoning-effort.test.ts +109 -0
  288. package/src/tests/refresh-modelsdev-carryforward.test.ts +5 -5
  289. package/src/tests/runner-key-completion-outcome.test.ts +112 -0
  290. package/src/tests/runner-key-outcome-late-reset.test.ts +224 -0
  291. package/src/tests/runner-registration-retry.test.ts +149 -0
  292. package/src/tests/runtime-model-catalog.test.ts +86 -0
  293. package/src/tests/script-connections.test.ts +9 -0
  294. package/src/tests/seat-mismatch-report.test.ts +85 -0
  295. package/src/tests/seed-scripts.test.ts +138 -0
  296. package/src/tests/seed.test.ts +25 -0
  297. package/src/tests/session-costs.test.ts +14 -11
  298. package/src/tests/slack-message-text.test.ts +101 -0
  299. package/src/tests/slack-render-v2.test.ts +96 -0
  300. package/src/tests/slack-session-status.test.ts +626 -0
  301. package/src/tests/slack-thread-buffer-context.test.ts +93 -0
  302. package/src/tests/status.test.ts +29 -0
  303. package/src/tests/steering-promotion.test.ts +51 -0
  304. package/src/tests/store-progress-ownership.test.ts +136 -0
  305. package/src/tests/subscription-plans.test.ts +28 -0
  306. package/src/tests/task-cascade-fail.test.ts +148 -2
  307. package/src/tests/task-citations.test.ts +3 -2
  308. package/src/tests/tool-annotations.test.ts +1 -1
  309. package/src/tests/usage-cache.test.ts +166 -0
  310. package/src/tests/workflow-agent-task.test.ts +30 -0
  311. package/src/tests/workflow-executors.test.ts +5 -2
  312. package/src/tests/workflow-system-one-decision-review.test.ts +1021 -0
  313. package/src/tests/workflow-system-one-decision-save-warnings.test.ts +365 -0
  314. package/src/tests/workflow-system-one-decision.test.ts +2063 -0
  315. package/src/tests/workflow-triggers-v2.test.ts +320 -0
  316. package/src/tools/model-catalog/index.ts +137 -0
  317. package/src/tools/schedules/create-schedule.ts +17 -2
  318. package/src/tools/schedules/patch-schedule.ts +31 -1
  319. package/src/tools/schedules/update-schedule.ts +31 -1
  320. package/src/tools/send-task.ts +18 -1
  321. package/src/tools/store-progress.ts +49 -4
  322. package/src/tools/task-action.ts +14 -1
  323. package/src/tools/tool-config.ts +3 -1
  324. package/src/tools/workflows/create-workflow.ts +17 -4
  325. package/src/tools/workflows/patch-workflow-node.ts +17 -7
  326. package/src/tools/workflows/patch-workflow.ts +9 -2
  327. package/src/tools/workflows/update-workflow.ts +17 -4
  328. package/src/types.ts +41 -1
  329. package/src/utils/codex-auth-failure.ts +34 -0
  330. package/src/utils/context-window.ts +47 -53
  331. package/src/utils/credentials.ts +72 -30
  332. package/src/utils/error-tracker.ts +80 -0
  333. package/src/utils/harness-cli-version.ts +115 -0
  334. package/src/utils/harness-model-error.ts +33 -0
  335. package/src/utils/model-rate-limit-windows.ts +12 -0
  336. package/src/utils/runtime-model-catalog.ts +152 -0
  337. package/src/utils/spawn-model-guard.ts +43 -0
  338. package/src/utils/subscription-plans.ts +22 -0
  339. package/src/workflows/approval-resolution.ts +39 -0
  340. package/src/workflows/definition.ts +7 -0
  341. package/src/workflows/engine.ts +69 -1
  342. package/src/workflows/executors/agent-task.ts +16 -6
  343. package/src/workflows/executors/base.ts +53 -0
  344. package/src/workflows/executors/human-in-the-loop.ts +8 -8
  345. package/src/workflows/executors/index.ts +1 -0
  346. package/src/workflows/executors/registry.ts +2 -0
  347. package/src/workflows/executors/system-one-decision.ts +1232 -0
  348. package/src/workflows/executors/system-one-providers.ts +301 -0
  349. package/src/workflows/executors/workflow-llm.ts +13 -1
  350. package/src/workflows/model-validation.ts +45 -0
  351. package/src/workflows/patch-definition.ts +5 -0
  352. package/src/workflows/readiness.ts +86 -0
  353. package/src/workflows/recovery.ts +12 -12
  354. package/src/workflows/resume.ts +23 -9
  355. package/src/workflows/triggers.ts +85 -0
  356. package/templates/schedules/daily-blocker-digest/content.md +4 -5
  357. package/templates/skills/agent-browser/SKILL.md +6 -5
  358. package/templates/skills/agent-browser/content.md +6 -5
  359. package/templates/skills/heartbeat-runbook/config.json +1 -1
  360. package/templates/skills/heartbeat-runbook/content.md +2 -2
  361. package/templates/skills/swarm-scripts/SKILL.md +1 -1
  362. package/templates/skills/swarm-scripts/content.md +1 -1
  363. package/templates/skills/workflow-iterate/content.md +82 -1
  364. package/templates/workflows/claude-code-changelog-watch/content.md +2 -0
  365. package/templates/workflows/pr-review-status-sweep/content.md +2 -0
  366. package/dist/boot-seeds-s3q01xj3.js +0 -26
  367. package/dist/claude-sdk-session-t442qz6f.js +0 -39108
  368. package/dist/cli-0xjkj1j7.js +0 -61
  369. package/dist/cli-20cj37vt.js +0 -335561
  370. package/dist/cli-2djtr51b.js +0 -48
  371. package/dist/cli-65hjmev5.js +0 -1141
  372. package/dist/cli-aw1jwp1r.js +0 -14075
  373. package/dist/cli-h967nb6r.js +0 -6603
  374. package/dist/cli-vapyk8sw.js +0 -20
  375. package/dist/cli-ws6rqycm.js +0 -247
  376. package/dist/pricing-refresh-v1cbr9te.js +0 -27
  377. package/src/providers/modelsdev-reasoning.json +0 -6453
  378. package/dist/{index-f24115ge.js → index-2wsdsnmm.js} +3 -3
package/src/be/db.ts CHANGED
@@ -124,6 +124,12 @@ import {
124
124
  SERVER_GENERATED_ATTACHMENT_CAPABILITY,
125
125
  SessionCostModelBreakdownSchema,
126
126
  } from "../types";
127
+ import {
128
+ CODEX_AUTH_FAILURE_BENCH_MS,
129
+ CODEX_AUTH_FAILURE_BENCH_THRESHOLD,
130
+ CODEX_AUTH_WATCH_NAMESPACE,
131
+ codexAuthBenchMarkerKey,
132
+ } from "../utils/codex-auth-failure";
127
133
  import { deriveProviderFromKeyType } from "../utils/credentials";
128
134
  import type { RateLimitWindowTelemetry } from "../utils/error-tracker";
129
135
  import { extractGitHubPullRequestUrls } from "../utils/github-pull-request";
@@ -136,7 +142,11 @@ import {
136
142
  import { activeModelBlock, type ModelFamily } from "../utils/model-rate-limit-windows";
137
143
  import { getCurrentRequestUserId } from "../utils/request-auth-context";
138
144
  import { registerVolatileSecret, scrubSecrets } from "../utils/secret-scrubber";
139
- import { estimateClaudePlan, SUBSCRIPTION_KEY_TYPES } from "../utils/subscription-plans";
145
+ import {
146
+ estimateClaudePlan,
147
+ planAllowsModelFamily,
148
+ SUBSCRIPTION_KEY_TYPES,
149
+ } from "../utils/subscription-plans";
140
150
  import { auditAssetKeys } from "./asset-key-audit";
141
151
  import { decryptSecret, encryptSecret, getEncryptionKey } from "./crypto";
142
152
  import { normalizeDate, normalizeDateRequired } from "./date-utils";
@@ -161,6 +171,7 @@ import {
161
171
  getLatestContextVersion,
162
172
  } from "./db/context-versions";
163
173
  import { getDb, getDbClient } from "./db/runtime";
174
+ import { classifyTaskHumanFree, reclassifyTaskHumanFree } from "./db/tasks/human-free";
164
175
  import {
165
176
  type AgentTaskRow,
166
177
  configureTaskReadDependencies,
@@ -187,6 +198,7 @@ export {
187
198
  getAgentById,
188
199
  getAgentDailyTaskCounts,
189
200
  getAgentHarnessProviders,
201
+ getAgentStatusCounts,
190
202
  getAllAgents,
191
203
  getLeadAgent,
192
204
  getRemainingCapacity,
@@ -212,6 +224,7 @@ export {
212
224
  updateAgentStatus,
213
225
  updateAgentStatusFromCapacity,
214
226
  } from "./db/agents";
227
+ export { recordKeySeatMismatch } from "./db/api-keys";
215
228
  export { type ApprovalRequestSummary, listApprovalRequestSummaries } from "./db/approvals";
216
229
  export {
217
230
  computeContentHash,
@@ -260,6 +273,7 @@ export {
260
273
  getRecentlyFinishedWorkerTasks,
261
274
  getSlackTasksMissingTree,
262
275
  getSupersededTasksWithoutResume,
276
+ getSupersededTasksWithUnsettledDependents,
263
277
  getTaskById,
264
278
  getTaskStats,
265
279
  getTasksByAgentId,
@@ -291,6 +305,7 @@ export {
291
305
  pauseTask,
292
306
  resetOrphanedInProgressTasksForAgent,
293
307
  resumeTask,
308
+ settleSupersededTaskDependents,
294
309
  startTask,
295
310
  supersedeTask,
296
311
  updateTaskClaudeSessionId,
@@ -891,6 +906,44 @@ export async function getSlackTasksInThread(
891
906
  return rows.map(rowToAgentTask);
892
907
  }
893
908
 
909
+ /**
910
+ * Slack threads with an open human request raised by one of their own tasks
911
+ * (`request-human-input`): the swarm cannot go on until a person answers.
912
+ * A request past its `expiresAt` is about to become a timeout follow-up, so
913
+ * it no longer counts as waiting.
914
+ */
915
+ export async function listSlackThreadsAwaitingHuman(): Promise<
916
+ Array<{ channelId: string; threadTs: string }>
917
+ > {
918
+ return getDbClient().query<{ channelId: string; threadTs: string }>(
919
+ `SELECT DISTINCT task.slackChannelId AS channelId, task.slackThreadTs AS threadTs
920
+ FROM approval_requests request
921
+ JOIN agent_tasks task ON task.id = request.sourceTaskId
922
+ WHERE request.status = 'pending'
923
+ AND (request.expiresAt IS NULL OR request.expiresAt > strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
924
+ AND task.slackChannelId IS NOT NULL
925
+ AND task.slackThreadTs IS NOT NULL`,
926
+ );
927
+ }
928
+
929
+ export async function isSlackThreadAwaitingHuman(
930
+ channelId: string,
931
+ threadTs: string,
932
+ ): Promise<boolean> {
933
+ const row = await getDbClient().get<{ waiting: number }>(
934
+ `SELECT 1 AS waiting
935
+ FROM approval_requests request
936
+ JOIN agent_tasks task ON task.id = request.sourceTaskId
937
+ WHERE request.status = 'pending'
938
+ AND (request.expiresAt IS NULL OR request.expiresAt > strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
939
+ AND task.slackChannelId = ?
940
+ AND task.slackThreadTs = ?
941
+ LIMIT 1`,
942
+ [channelId, threadTs],
943
+ );
944
+ return !!row;
945
+ }
946
+
894
947
  export async function markTaskSlackReplySent(taskId: string): Promise<void> {
895
948
  await getDbClient().run(`UPDATE agent_tasks SET slackReplySent = 1 WHERE id = ?`, [taskId]);
896
949
  }
@@ -2703,6 +2756,16 @@ export async function createTaskExtended(
2703
2756
  logRoutingDecisionFailure(id, "capture", error);
2704
2757
  }
2705
2758
 
2759
+ const isHumanFree = await classifyTaskHumanFree({
2760
+ taskType: options?.taskType,
2761
+ tags: JSON.stringify(options?.tags ?? []),
2762
+ source: options?.source ?? "mcp",
2763
+ requestedByUserId: options?.requestedByUserId,
2764
+ requestedByUserIdInherited,
2765
+ parentTaskId: options?.parentTaskId,
2766
+ workflowRunId: options?.workflowRunId,
2767
+ });
2768
+
2706
2769
  const inserted = await getDbClient().get<AgentTaskRow>(
2707
2770
  `INSERT INTO agent_tasks (
2708
2771
  id, "key", agentId, creatorAgentId, task, status, source, routing_reason, routing_source, routing_note,
@@ -2712,8 +2775,8 @@ export async function createTaskExtended(
2712
2775
  vcsInstallationId, vcsNodeId,
2713
2776
  agentmailInboxId, agentmailMessageId, agentmailThreadId,
2714
2777
  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 *`,
2778
+ workflowRunId, workflowRunStepId, outputSchema, followUpConfig, requestedByUserId, requestedByUserIdInherited, contextKey, routingAffinity, swarmVersion, createdAt, lastUpdatedAt, created_by, updated_by, isHumanFree
2779
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
2717
2780
  [
2718
2781
  id,
2719
2782
  assetKey,
@@ -2768,6 +2831,7 @@ export async function createTaskExtended(
2768
2831
  now,
2769
2832
  auditUserId,
2770
2833
  auditUserId,
2834
+ isHumanFree ? 1 : 0,
2771
2835
  ],
2772
2836
  );
2773
2837
  if (!inserted) throw new Error("Failed to create task");
@@ -3318,9 +3382,15 @@ const ELIGIBILITY_SCAN_CAP = Number(process.env.ELIGIBILITY_SCAN_CAP) || 500;
3318
3382
  * JSON-parsing `routingAffinity` in SQL) until `limit` eligible tasks are
3319
3383
  * found or the pool is exhausted, capped at `ELIGIBILITY_SCAN_CAP` rows
3320
3384
  * scanned so a pool full of ineligible tasks can't turn every poll into an
3321
- * unbounded scan.
3385
+ * unbounded scan. `accept` adds a caller-side filter (e.g. harness model
3386
+ * compatibility) that runs inside the scan, so rejected rows do not use up
3387
+ * the `limit`.
3322
3388
  */
3323
- export async function getUnassignedTaskIdsForAgent(agentId: string, limit = 10): Promise<string[]> {
3389
+ export async function getUnassignedTaskIdsForAgent(
3390
+ agentId: string,
3391
+ limit = 10,
3392
+ accept?: (task: AgentTask) => boolean | Promise<boolean>,
3393
+ ): Promise<string[]> {
3324
3394
  const agent = await getAgentById(agentId);
3325
3395
  if (!agent) return [];
3326
3396
 
@@ -3337,7 +3407,7 @@ export async function getUnassignedTaskIdsForAgent(agentId: string, limit = 10):
3337
3407
 
3338
3408
  for (const row of rows) {
3339
3409
  const task = rowToAgentTask(row);
3340
- if (isAgentEligibleForTask(agent, task)) {
3410
+ if (isAgentEligibleForTask(agent, task) && (!accept || (await accept(task)))) {
3341
3411
  eligible.push(task.id);
3342
3412
  if (eligible.length >= limit) break;
3343
3413
  }
@@ -4893,60 +4963,12 @@ export interface SessionCostByUserRow {
4893
4963
  /** `opts.userId` sentinel selecting spend with no human requester. */
4894
4964
  export const UNATTRIBUTED_USER_ID = "unattributed";
4895
4965
 
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
- )`;
4966
+ /**
4967
+ * True for a session whose task is structurally human-free (`isHumanFree`, set
4968
+ * when the task is created; rule in `src/be/db/tasks/human-free.ts`). A session
4969
+ * with no task row is not human-free.
4970
+ */
4971
+ const HUMAN_FREE_SQL = "(COALESCE(t.isHumanFree, 0) = 1)";
4950
4972
 
4951
4973
  export async function getSessionCostSummary(opts: {
4952
4974
  startDate?: string;
@@ -4966,19 +4988,15 @@ export async function getSessionCostSummary(opts: {
4966
4988
  // re-attributed after the fact, so the human requester is resolved by joining
4967
4989
  // through the task (same shape as `getDailySpendForUser`). Every column is
4968
4990
  // `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.
4991
+ // INDEXED BY: without ANALYZE statistics (production has none) the planner
4992
+ // probes tasks through the primary key and reads each full row, which follows
4993
+ // the task's overflow pages. The covering index answers the same probe from
4994
+ // the index alone (migration 182).
4995
+ const from =
4996
+ "FROM session_costs sc LEFT JOIN agent_tasks t INDEXED BY idx_agent_tasks_usage_cover ON t.id = sc.taskId";
4997
+ // Only a user filter reads the task, and the join adds no rows (task id is unique).
4998
+ const agentFrom = opts.userId === undefined ? "FROM session_costs sc" : from;
4999
+
4982
5000
  const conditions: string[] = [];
4983
5001
  const params: string[] = [];
4984
5002
 
@@ -5002,11 +5020,6 @@ export async function getSessionCostSummary(opts: {
5002
5020
  }
5003
5021
 
5004
5022
  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
5023
  const subscriptionSql = `t.credentialKeyType IN (${SUBSCRIPTION_KEY_TYPES.map((k) => `'${k}'`).join(", ")})`;
5011
5024
 
5012
5025
  // Totals
@@ -5026,8 +5039,7 @@ export async function getSessionCostSummary(opts: {
5026
5039
  };
5027
5040
 
5028
5041
  const totalsRow = await getDbClient().get<TotalsRow>(
5029
- `WITH RECURSIVE ${HUMAN_FREE_TASKS_CTE}
5030
- SELECT
5042
+ `SELECT
5031
5043
  COALESCE(SUM(sc.totalCostUsd), 0) as totalCostUsd,
5032
5044
  COALESCE(SUM(sc.inputTokens), 0) as totalInputTokens,
5033
5045
  COALESCE(SUM(sc.outputTokens), 0) as totalOutputTokens,
@@ -5044,7 +5056,7 @@ export async function getSessionCostSummary(opts: {
5044
5056
  COUNT(DISTINCT CASE WHEN ${HUMAN_FREE_SQL} THEN t.id END) as excludedTaskCount,
5045
5057
  COALESCE(SUM(CASE WHEN ${subscriptionSql}
5046
5058
  THEN sc.totalCostUsd ELSE 0 END), 0) as subscriptionCostUsd
5047
- ${fromHf} ${where}`,
5059
+ ${from} ${where}`,
5048
5060
  params,
5049
5061
  );
5050
5062
 
@@ -5082,8 +5094,7 @@ export async function getSessionCostSummary(opts: {
5082
5094
  sessions: number;
5083
5095
  subscriptionCostUsd: number;
5084
5096
  }>(
5085
- `${withHf}
5086
- SELECT
5097
+ `SELECT
5087
5098
  DATE(sc.createdAt) as date,
5088
5099
  COALESCE(SUM(sc.totalCostUsd), 0) as costUsd,
5089
5100
  COALESCE(SUM(sc.inputTokens), 0) as inputTokens,
@@ -5091,7 +5102,7 @@ export async function getSessionCostSummary(opts: {
5091
5102
  COUNT(*) as sessions,
5092
5103
  COALESCE(SUM(CASE WHEN ${subscriptionSql} THEN sc.totalCostUsd ELSE 0 END), 0)
5093
5104
  as subscriptionCostUsd
5094
- ${scopedFrom} ${where}
5105
+ ${from} ${where}
5095
5106
  GROUP BY DATE(sc.createdAt)
5096
5107
  ORDER BY date ASC`,
5097
5108
  params,
@@ -5109,15 +5120,14 @@ export async function getSessionCostSummary(opts: {
5109
5120
  sessions: number;
5110
5121
  durationMs: number;
5111
5122
  }>(
5112
- `${withHf}
5113
- SELECT
5123
+ `SELECT
5114
5124
  sc.agentId as agentId,
5115
5125
  COALESCE(SUM(sc.totalCostUsd), 0) as costUsd,
5116
5126
  COALESCE(SUM(sc.inputTokens), 0) as inputTokens,
5117
5127
  COALESCE(SUM(sc.outputTokens), 0) as outputTokens,
5118
5128
  COUNT(*) as sessions,
5119
5129
  COALESCE(SUM(sc.durationMs), 0) as durationMs
5120
- ${scopedFrom} ${where}
5130
+ ${agentFrom} ${where}
5121
5131
  GROUP BY sc.agentId
5122
5132
  ORDER BY costUsd DESC`,
5123
5133
  params,
@@ -5129,15 +5139,14 @@ export async function getSessionCostSummary(opts: {
5129
5139
  let byUser: SessionCostByUserRow[] = [];
5130
5140
  if (groupBy === "user" || groupBy === "both") {
5131
5141
  byUser = await getDbClient().query<SessionCostByUserRow>(
5132
- `WITH RECURSIVE ${HUMAN_FREE_TASKS_CTE}
5133
- SELECT
5142
+ `SELECT
5134
5143
  CASE WHEN ${HUMAN_FREE_SQL} THEN NULL ELSE t.requestedByUserId END as userId,
5135
5144
  COALESCE(SUM(sc.totalCostUsd), 0) as costUsd,
5136
5145
  COALESCE(SUM(sc.inputTokens), 0) as inputTokens,
5137
5146
  COALESCE(SUM(sc.outputTokens), 0) as outputTokens,
5138
5147
  COUNT(DISTINCT sc.taskId) as tasks,
5139
5148
  COALESCE(SUM(sc.durationMs), 0) as durationMs
5140
- ${fromHf} ${where}
5149
+ ${from} ${where}
5141
5150
  GROUP BY CASE WHEN ${HUMAN_FREE_SQL} THEN NULL ELSE t.requestedByUserId END
5142
5151
  ORDER BY costUsd DESC`,
5143
5152
  params,
@@ -5151,8 +5160,7 @@ export async function getSessionCostSummary(opts: {
5151
5160
  "name" | "subscription" | "plan" | "planSource"
5152
5161
  >;
5153
5162
  const rows = await getDbClient().query<CredentialRow>(
5154
- `${withHf}
5155
- SELECT
5163
+ `SELECT
5156
5164
  t.credentialKeyType as keyType,
5157
5165
  t.credentialKeySuffix as keySuffix,
5158
5166
  COALESCE(SUM(sc.totalCostUsd), 0) as costUsd,
@@ -5161,7 +5169,7 @@ export async function getSessionCostSummary(opts: {
5161
5169
  COUNT(*) as sessions,
5162
5170
  MIN(sc.createdAt) as firstSessionAt,
5163
5171
  MAX(sc.createdAt) as lastSessionAt
5164
- ${scopedFrom} ${where}
5172
+ ${from} ${where}
5165
5173
  GROUP BY t.credentialKeyType, t.credentialKeySuffix
5166
5174
  ORDER BY costUsd DESC`,
5167
5175
  params,
@@ -5223,18 +5231,6 @@ async function getCredentialLabels(): Promise<
5223
5231
  return labels;
5224
5232
  }
5225
5233
 
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
5234
  // --- Per-person attribution (four-metric view) ---
5239
5235
 
5240
5236
  export interface AttributionByPersonRow {
@@ -5265,7 +5261,7 @@ export interface AttributionByPersonRow {
5265
5261
  * a composite score. Scope is root tasks (`problemsInitiated`/`problemsShipped`)
5266
5262
  * or the person's entire task tree (`*Reached`); `requestedByUserId IS NULL`
5267
5263
  * (autonomous work) and structurally-human-free rows with a stale/inherited
5268
- * requester (see `HUMAN_FREE_SQL` in `getSessionCostSummary`) are excluded —
5264
+ * requester (`agent_tasks.isHumanFree`, see `src/be/db/tasks/human-free.ts`) are excluded —
5269
5265
  * neither belongs to a person.
5270
5266
  *
5271
5267
  * "Problems shipped" detection walks each root's full task tree, preferring a
@@ -5309,18 +5305,20 @@ export async function getAttributionByPerson(opts: {
5309
5305
 
5310
5306
  type RootRow = { userId: string; initiated: number; shipped: number };
5311
5307
  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
5308
+ `WITH RECURSIVE selected_roots(id, requestedByUserId, status) AS (
5309
+ SELECT t.id, t.requestedByUserId, t.status
5314
5310
  FROM agent_tasks t
5315
- ${where} AND t.parentTaskId IS NULL AND NOT ${ROOT_HUMAN_FREE_SQL}
5311
+ ${where} AND t.parentTaskId IS NULL AND t.isHumanFree = 0
5316
5312
  ),
5317
- task_tree(rootId, taskId, output) AS (
5318
- SELECT id, id, output
5313
+ -- Ids only: carrying each descendant's \`output\` through the recursion copied
5314
+ -- every task's text into the queue. The output fallback below reads it by id.
5315
+ task_tree(rootId, taskId) AS (
5316
+ SELECT id, id
5319
5317
  FROM selected_roots
5320
5318
 
5321
5319
  UNION ALL
5322
5320
 
5323
- SELECT tree.rootId, child.id, child.output
5321
+ SELECT tree.rootId, child.id
5324
5322
  FROM agent_tasks child
5325
5323
  JOIN task_tree tree ON child.parentTaskId = tree.taskId
5326
5324
  ),
@@ -5353,8 +5351,11 @@ export async function getAttributionByPerson(opts: {
5353
5351
 
5354
5352
  SELECT tree.rootId
5355
5353
  FROM task_tree tree
5356
- WHERE tree.output LIKE '%github.com/%/pull/%'
5357
- OR tree.output LIKE '%/-/merge_requests/%'
5354
+ -- CROSS JOIN pins the order (tree outer, task by id). Left to itself the
5355
+ -- planner scans every task's output and probes the tree instead.
5356
+ CROSS JOIN agent_tasks described ON described.id = tree.taskId
5357
+ WHERE described.output LIKE '%github.com/%/pull/%'
5358
+ OR described.output LIKE '%/-/merge_requests/%'
5358
5359
  )
5359
5360
  SELECT
5360
5361
  t.requestedByUserId as userId,
@@ -5373,65 +5374,13 @@ export async function getAttributionByPerson(opts: {
5373
5374
  surfacesReached: number;
5374
5375
  };
5375
5376
  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
5377
+ `SELECT
5429
5378
  t.requestedByUserId as userId,
5430
5379
  COUNT(DISTINCT t.agentId) as agentsReached,
5431
5380
  COUNT(DISTINCT t.vcsRepo) as reposReached,
5432
5381
  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)
5382
+ FROM agent_tasks t
5383
+ ${where} AND t.isHumanFree = 0
5435
5384
  GROUP BY t.requestedByUserId`,
5436
5385
  params,
5437
5386
  );
@@ -7596,7 +7545,19 @@ export async function updateWorkflow(
7596
7545
  }
7597
7546
 
7598
7547
  export async function deleteWorkflow(id: string, source?: "api" | "mcp"): Promise<boolean> {
7548
+ // One transaction: the stored human-free flags must never lag the deleted runs
7549
+ // (a scheduled workflow root is human-free only while its run row exists).
7550
+ return await getDbClient().transaction(() => deleteWorkflowRows(id, source));
7551
+ }
7552
+
7553
+ async function deleteWorkflowRows(id: string, source?: "api" | "mcp"): Promise<boolean> {
7599
7554
  const client = getDbClient();
7555
+ const linkedTaskIds = (
7556
+ await client.query<{ id: string }>(
7557
+ `SELECT id FROM agent_tasks WHERE workflowRunId IN (SELECT id FROM workflow_runs WHERE workflowId = ?)`,
7558
+ [id],
7559
+ )
7560
+ ).map((row) => row.id);
7600
7561
  // Cascade delete in FK-safe order:
7601
7562
  // 1. Unlink agent_tasks (they reference steps and runs)
7602
7563
  await client.run(
@@ -7610,6 +7571,7 @@ export async function deleteWorkflow(id: string, source?: "api" | "mcp"): Promis
7610
7571
  );
7611
7572
  // 3. Delete runs (they reference workflow)
7612
7573
  await client.run("DELETE FROM workflow_runs WHERE workflowId = ?", [id]);
7574
+ await reclassifyTaskHumanFree(linkedTaskIds);
7613
7575
  // 4. Delete workflow
7614
7576
  const result = await client.run("DELETE FROM workflows WHERE id = ?", [id]);
7615
7577
  const deleted = result.changes > 0;
@@ -11430,6 +11392,13 @@ export interface ApiKeyStatus {
11430
11392
  /** Subscription plan id (`SUBSCRIPTION_PLANS`), when known. */
11431
11393
  plan: string | null;
11432
11394
  planSource: PlanSource | null;
11395
+ /** When the CLI last rejected a model with `credits_required` on this key. */
11396
+ lastSeatMismatchAt: string | null;
11397
+ /** Model family of that rejection (`fable`, `opus`, ...). */
11398
+ lastSeatMismatchModel: string | null;
11399
+ /** Auth failures in a row since the last success or clear. */
11400
+ consecutiveAuthFailures: number;
11401
+ lastAuthFailureAt: string | null;
11433
11402
  createdAt: string;
11434
11403
  updatedAt: string;
11435
11404
  }
@@ -11459,6 +11428,9 @@ export interface AvailableKeyIndicesResult {
11459
11428
  modelBlockedIndices: number[];
11460
11429
  /** ISO of the earliest resetsAt among modelBlockedIndices, or null when none. */
11461
11430
  earliestModelResetAt: string | null;
11431
+ /** Indices excluded because the key's subscription plan cannot run the model family. */
11432
+ seatBlockedIndices: number[];
11433
+ authFailureFence: number; // highest `authFailureSeq` among these rows
11462
11434
  }
11463
11435
 
11464
11436
  /**
@@ -11468,7 +11440,10 @@ export interface AvailableKeyIndicesResult {
11468
11440
  * When `modelFamily` has a weekly window (fable/opus/sonnet), a key whose
11469
11441
  * `rateLimitWindows` carries an active rejected window for that family is
11470
11442
  * excluded from `availableIndices` and reported in `modelBlockedIndices`
11471
- * instead — the key itself stays `available` for every other model.
11443
+ * instead — the key itself stays `available` for every other model. A key
11444
+ * whose `plan` cannot run the family (`planAllowsModelFamily`) is excluded
11445
+ * and reported in `seatBlockedIndices`, also when it is key-wide or
11446
+ * model-window blocked; a seat block has no reset time.
11472
11447
  */
11473
11448
  export async function getAvailableKeyIndices(
11474
11449
  keyType: string,
@@ -11482,21 +11457,23 @@ export async function getAvailableKeyIndices(
11482
11457
  const client = getDbClient();
11483
11458
  const effectiveScopeId = scopeId ?? "";
11484
11459
 
11485
- // Auto-clear expired rate limits
11460
+ // Auto-clear expired rate limits. Never an auth bench: only `clearAuthBench` lifts it.
11486
11461
  await client.run(
11487
11462
  `UPDATE api_key_status
11488
11463
  SET status = 'available', rateLimitedUntil = NULL, updatedAt = ?
11489
11464
  WHERE keyType = ? AND scope = ? AND scopeId = ?
11490
- AND status = 'rate_limited' AND rateLimitedUntil IS NOT NULL AND rateLimitedUntil <= ?`,
11491
- [now, keyType, scope, effectiveScopeId, now],
11465
+ AND status = 'rate_limited' AND rateLimitedUntil <= ? AND consecutiveAuthFailures < ?`,
11466
+ [now, keyType, scope, effectiveScopeId, now, CODEX_AUTH_FAILURE_BENCH_THRESHOLD],
11492
11467
  );
11493
11468
 
11494
11469
  const rows = await client.query<{
11495
11470
  keyIndex: number;
11496
11471
  status: string;
11497
11472
  rateLimitWindows: string | null;
11473
+ plan: string | null;
11474
+ authFailureSeq: number;
11498
11475
  }>(
11499
- `SELECT keyIndex, status, rateLimitWindows FROM api_key_status
11476
+ `SELECT keyIndex, status, rateLimitWindows, plan, authFailureSeq FROM api_key_status
11500
11477
  WHERE keyType = ? AND scope = ? AND scopeId = ?`,
11501
11478
  [keyType, scope, effectiveScopeId],
11502
11479
  );
@@ -11527,19 +11504,32 @@ export async function getAvailableKeyIndices(
11527
11504
  }
11528
11505
  const modelBlockedSet = new Set(modelBlockedIndices);
11529
11506
 
11507
+ // A seat block is a plan fact: report it even when the key is also key-wide
11508
+ // or model-window blocked, or the temporary block hides it from admission
11509
+ // and the fallback pick can select a key that cannot run the model.
11510
+ const seatBlockedIndices: number[] = [];
11511
+ if (modelFamily) {
11512
+ for (const row of rows) {
11513
+ if (!planAllowsModelFamily(row.plan, modelFamily)) seatBlockedIndices.push(row.keyIndex);
11514
+ }
11515
+ }
11516
+ const seatBlockedSet = new Set(seatBlockedIndices);
11517
+
11530
11518
  const availableIndices: number[] = [];
11531
11519
  for (let i = 0; i < totalKeys; i++) {
11532
- if (blockedIndices.has(i) || modelBlockedSet.has(i)) continue;
11520
+ if (blockedIndices.has(i) || modelBlockedSet.has(i) || seatBlockedSet.has(i)) continue;
11533
11521
  availableIndices.push(i);
11534
11522
  }
11535
11523
 
11536
11524
  return {
11537
11525
  availableIndices,
11538
11526
  modelBlockedIndices,
11527
+ seatBlockedIndices,
11539
11528
  earliestModelResetAt:
11540
11529
  earliestModelResetsAtSec !== undefined
11541
11530
  ? new Date(earliestModelResetsAtSec * 1000).toISOString()
11542
11531
  : null,
11532
+ authFailureFence: Math.max(0, ...rows.map((r) => r.authFailureSeq)),
11543
11533
  };
11544
11534
  }
11545
11535
 
@@ -11627,9 +11617,7 @@ export async function setApiKeyPlan(
11627
11617
  return result.changes > 0;
11628
11618
  }
11629
11619
 
11630
- /**
11631
- * Mark a key as rate-limited with a retry-after timestamp.
11632
- */
11620
+ /** Mark a key as rate-limited until a retry-after timestamp; never shortens an auth-failure bench. */
11633
11621
  export async function markKeyRateLimited(
11634
11622
  keyType: string,
11635
11623
  keySuffix: string,
@@ -11647,7 +11635,7 @@ export async function markKeyRateLimited(
11647
11635
  ON CONFLICT(keyType, keySuffix, scope, scopeId)
11648
11636
  DO UPDATE SET
11649
11637
  status = 'rate_limited',
11650
- rateLimitedUntil = excluded.rateLimitedUntil,
11638
+ rateLimitedUntil = CASE WHEN status = 'rate_limited' AND consecutiveAuthFailures >= ${CODEX_AUTH_FAILURE_BENCH_THRESHOLD} THEN MAX(COALESCE(rateLimitedUntil, ''), excluded.rateLimitedUntil) ELSE excluded.rateLimitedUntil END,
11651
11639
  lastRateLimitAt = excluded.lastRateLimitAt,
11652
11640
  rateLimitCount = rateLimitCount + 1,
11653
11641
  keyIndex = excluded.keyIndex,
@@ -11656,6 +11644,63 @@ export async function markKeyRateLimited(
11656
11644
  );
11657
11645
  }
11658
11646
 
11647
+ export interface KeyAuthFailureResult {
11648
+ consecutiveAuthFailures: number;
11649
+ benched: boolean;
11650
+ rateLimitedUntil: string | null;
11651
+ }
11652
+
11653
+ /** Count an auth failure, ordered by `authFailureSeq`; at the threshold, bench (extend only). */
11654
+ export async function recordKeyAuthFailure(
11655
+ keyType: string,
11656
+ keySuffix: string,
11657
+ keyIndex: number,
11658
+ scope = "global",
11659
+ scopeId: string | null = null,
11660
+ ): Promise<KeyAuthFailureResult> {
11661
+ const effectiveScopeId = scopeId ?? "";
11662
+ const provider = deriveProviderFromKeyType(keyType);
11663
+ return await getDbClient().transaction(async (tx) => {
11664
+ const now = new Date().toISOString();
11665
+ const source = "report-auth-failure";
11666
+ const row = await tx.get<{ consecutiveAuthFailures: number; rateLimitedUntil: string | null }>(
11667
+ `INSERT INTO api_key_status (keyType, keySuffix, keyIndex, scope, scopeId, status, consecutiveAuthFailures, lastAuthFailureAt, authFailureSeq, provider, updatedAt)
11668
+ VALUES (?, ?, ?, ?, ?, 'available', 1, ?, (SELECT COALESCE(MAX(authFailureSeq), 0) + 1 FROM api_key_status), ?, ?)
11669
+ ON CONFLICT(keyType, keySuffix, scope, scopeId)
11670
+ DO UPDATE SET
11671
+ consecutiveAuthFailures = consecutiveAuthFailures + 1,
11672
+ lastAuthFailureAt = excluded.lastAuthFailureAt, authFailureSeq = excluded.authFailureSeq,
11673
+ keyIndex = excluded.keyIndex, updatedAt = excluded.updatedAt
11674
+ RETURNING consecutiveAuthFailures, rateLimitedUntil`,
11675
+ [keyType, keySuffix, keyIndex, scope, effectiveScopeId, now, provider, now],
11676
+ );
11677
+ if (!row) throw new Error("Failed to record auth failure");
11678
+ const { consecutiveAuthFailures } = row;
11679
+ if (consecutiveAuthFailures < CODEX_AUTH_FAILURE_BENCH_THRESHOLD) {
11680
+ return { consecutiveAuthFailures, benched: false, rateLimitedUntil: row.rateLimitedUntil };
11681
+ }
11682
+ let benchedUntil = new Date(Date.parse(now) + CODEX_AUTH_FAILURE_BENCH_MS).toISOString();
11683
+ if (row.rateLimitedUntil && Date.parse(row.rateLimitedUntil) > Date.parse(benchedUntil)) {
11684
+ benchedUntil = row.rateLimitedUntil;
11685
+ }
11686
+ await tx.run(
11687
+ `UPDATE api_key_status
11688
+ SET status = 'rate_limited', rateLimitedUntil = ?, lastRateLimitAt = ?,
11689
+ rateLimitCount = rateLimitCount + 1, updatedAt = ?
11690
+ WHERE keyType = ? AND keySuffix = ? AND scope = ? AND scopeId = ?`,
11691
+ [benchedUntil, now, now, keyType, keySuffix, scope, effectiveScopeId],
11692
+ );
11693
+ await upsertKv({
11694
+ namespace: CODEX_AUTH_WATCH_NAMESPACE,
11695
+ key: codexAuthBenchMarkerKey(keySuffix),
11696
+ valueType: "json",
11697
+ value: { keyIndex, keyType, benchedAt: now, benchedUntil, lastFailAt: now, source },
11698
+ expiresAt: Date.parse(benchedUntil) + 86_400_000,
11699
+ });
11700
+ return { consecutiveAuthFailures, benched: true, rateLimitedUntil: benchedUntil };
11701
+ });
11702
+ }
11703
+
11659
11704
  /**
11660
11705
  * Merges reported window snapshots into the stored ones, one window type at a
11661
11706
  * time. A reported entry replaces the stored entry unless the stored entry was
@@ -11797,24 +11842,48 @@ export async function setApiKeyName(
11797
11842
  }
11798
11843
 
11799
11844
  /**
11800
- * Clear a stale rate-limit record after a successful use proves the key is healthy.
11845
+ * Clear a stale rate-limit record after a successful use proves the key is healthy. With
11846
+ * `clearAuthBench` + `authFence` (the `authFailureFence` read before the task or credential write),
11847
+ * also lift an auth bench, reset the count and delete the Codex bench marker, but never for a
11848
+ * failure recorded after the fence. `keyIndex` (slot re-login) also retires the slot's other logins.
11801
11849
  */
11802
11850
  export async function clearKeyRateLimit(
11803
11851
  keyType: string,
11804
11852
  keySuffix: string,
11805
11853
  scope = "global",
11806
11854
  scopeId: string | null = null,
11855
+ opts: { clearAuthBench?: boolean; keyIndex?: number; authFence?: number } = {},
11807
11856
  ): Promise<boolean> {
11808
11857
  const now = new Date().toISOString();
11809
11858
  const effectiveScopeId = scopeId ?? "";
11810
- const result = await getDbClient().run(
11811
- `UPDATE api_key_status
11812
- SET status = 'available', rateLimitedUntil = NULL, updatedAt = ?
11813
- WHERE keyType = ? AND keySuffix = ? AND scope = ? AND scopeId = ?
11814
- AND status = 'rate_limited'`,
11815
- [now, keyType, keySuffix, scope, effectiveScopeId],
11816
- );
11817
- return result.changes > 0;
11859
+ const { keyIndex = -1, authFence = null } = opts;
11860
+ const clearAuthBench = opts.clearAuthBench === true && authFence !== null;
11861
+ const key = [keyType, keySuffix, scope, effectiveScopeId];
11862
+ return await getDbClient().transaction(async (tx) => {
11863
+ const result = await tx.run(
11864
+ `UPDATE api_key_status
11865
+ SET status = 'available', rateLimitedUntil = NULL, updatedAt = ?
11866
+ WHERE keyType = ? AND keySuffix = ? AND scope = ? AND scopeId = ?
11867
+ AND status = 'rate_limited' AND ((? AND authFailureSeq <= ?) OR consecutiveAuthFailures < ?)`,
11868
+ [now, ...key, clearAuthBench ? 1 : 0, authFence, CODEX_AUTH_FAILURE_BENCH_THRESHOLD],
11869
+ );
11870
+ const cleared = result.changes > 0;
11871
+ if (!clearAuthBench) return cleared;
11872
+ const lifted = await tx.query<{ keySuffix: string }>(
11873
+ `UPDATE api_key_status SET consecutiveAuthFailures = 0, updatedAt = ?1,
11874
+ status = CASE WHEN keySuffix = ?2 THEN status ELSE 'available' END, rateLimitedUntil = CASE WHEN keySuffix = ?2 THEN rateLimitedUntil END
11875
+ WHERE keyType = ?3 AND scope = ?4 AND scopeId = ?5 AND (consecutiveAuthFailures > 0 OR status = 'rate_limited')
11876
+ AND ((keySuffix = ?2 AND authFailureSeq <= ?7) OR (keySuffix != ?2 AND keyIndex = ?6)) RETURNING keySuffix`,
11877
+ [now, keySuffix, keyType, scope, effectiveScopeId, keyIndex, authFence],
11878
+ );
11879
+ const retired = lifted.filter((row) => row.keySuffix !== keySuffix);
11880
+ if (keyType === "CODEX_OAUTH") {
11881
+ for (const suffix of [...(cleared ? [keySuffix] : []), ...retired.map((r) => r.keySuffix)]) {
11882
+ await deleteKv(CODEX_AUTH_WATCH_NAMESPACE, codexAuthBenchMarkerKey(suffix));
11883
+ }
11884
+ }
11885
+ return cleared || retired.length > 0;
11886
+ });
11818
11887
  }
11819
11888
 
11820
11889
  /**
@@ -12112,6 +12181,21 @@ export async function deleteUser(id: string, replacementUserId?: string): Promis
12112
12181
  )`,
12113
12182
  );
12114
12183
  const replacement = replacementUserId ?? null;
12184
+ // The human-free rule only asks whether a task's requester or its workflow
12185
+ // run's creator is NULL, so only clearing them (no replacement) can change a
12186
+ // stored flag. Gather the tasks now: after the rewrite nothing identifies
12187
+ // them. The workflow branch covers roots whose requester was never set.
12188
+ const reclassifySeedIds: string[] = replacementUserId
12189
+ ? []
12190
+ : (
12191
+ await tx.query<{ id: string }>(
12192
+ `SELECT id FROM agent_tasks WHERE requestedByUserId = ?
12193
+ UNION
12194
+ SELECT id FROM agent_tasks
12195
+ WHERE workflowRunId IN (SELECT id FROM workflow_runs WHERE created_by = ?)`,
12196
+ [id, id],
12197
+ )
12198
+ ).map((row) => row.id);
12115
12199
  for (const reference of references) {
12116
12200
  const table = quoteSqlIdentifier(reference.tableName);
12117
12201
  const column = quoteSqlIdentifier(reference.columnName);
@@ -12139,6 +12223,8 @@ export async function deleteUser(id: string, replacementUserId?: string): Promis
12139
12223
  );
12140
12224
  }
12141
12225
 
12226
+ await reclassifyTaskHumanFree(reclassifySeedIds);
12227
+
12142
12228
  const result = await tx.run("DELETE FROM users WHERE id = ?", [id]);
12143
12229
  return result.changes > 0;
12144
12230
  });