@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
@@ -10,6 +10,7 @@ import type { Seeder, SeederResult, SeederRunOptions } from "./types";
10
10
  /**
11
11
  * Apply one seeder. Idempotent and version-aware:
12
12
  * - upstream absent -> create
13
+ * - upstream identical to src -> no-op (re-baselines seed state)
13
14
  * - upstream pristine, src moved -> update
14
15
  * - upstream pristine, src same -> no-op
15
16
  * - upstream user-modified -> preserve (never overwrite)
@@ -38,12 +39,25 @@ export async function runSeeder(seeder: Seeder, opts?: SeederRunOptions): Promis
38
39
  }
39
40
 
40
41
  const state = await getSeedState(seeder.kind, item.key);
42
+
43
+ if (upstream === item.contentHash) {
44
+ // Live copy is byte-identical to the current source, so there is nothing
45
+ // user-authored to lose. Treat it as in sync regardless of seed state:
46
+ // adopt an unrecorded entity, and re-baseline one whose recorded hash
47
+ // drifted (edited, then reset to source) so the next source change is
48
+ // detectable as an update instead of being preserved forever.
49
+ if (state?.seededHash !== item.contentHash) {
50
+ await recordSeedState(seeder.kind, item.key, item.contentHash);
51
+ }
52
+ result.skippedUnchanged += 1;
53
+ continue;
54
+ }
55
+
41
56
  // "Pristine" = the live upstream copy still matches what we last seeded.
42
57
  // With no recorded state (first run after this framework landed, or a
43
- // pre-existing entity) we can only treat it as pristine when it is
44
- // byte-identical to the source — otherwise we conservatively assume a
45
- // user authored it and must not be clobbered.
46
- const pristine = state ? upstream === state.seededHash : upstream === item.contentHash;
58
+ // pre-existing entity) the live copy already differs from source here, so
59
+ // we conservatively assume a user authored it and must not clobber it.
60
+ const pristine = state ? upstream === state.seededHash : false;
47
61
 
48
62
  if (!pristine) {
49
63
  // A user changed the upstream copy since our last seed — preserve it.
@@ -51,14 +65,6 @@ export async function runSeeder(seeder: Seeder, opts?: SeederRunOptions): Promis
51
65
  continue;
52
66
  }
53
67
 
54
- if (upstream === item.contentHash) {
55
- // Neither side changed. Adopt an unrecorded-but-identical entity so the
56
- // next source change is correctly detectable as an update.
57
- if (!state) await recordSeedState(seeder.kind, item.key, item.contentHash);
58
- result.skippedUnchanged += 1;
59
- continue;
60
- }
61
-
62
68
  // Pristine upstream + changed source -> update to the new source version.
63
69
  await seeder.apply(item, "update", opts);
64
70
  await recordSeedState(seeder.kind, item.key, item.contentHash);
@@ -9,6 +9,7 @@
9
9
  *
10
10
  * Versioning rule (per item, evaluated against the `seed_state` table):
11
11
  * - upstream entity absent -> create
12
+ * - upstream identical to source -> no-op (seed state re-baselined)
12
13
  * - upstream pristine + source changed -> update
13
14
  * - upstream pristine + source same -> no-op
14
15
  * - upstream user-modified -> never overwrite (preserve)
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { publishCatalogReportPage } from "./catalog-report";
3
3
 
4
+ const MAX_TASK_SCAN_LIMIT = 500;
5
+
4
6
  export const argsSchema = z.object({
5
7
  days: z
6
8
  .number()
@@ -16,8 +18,9 @@ export const argsSchema = z.object({
16
18
  .number()
17
19
  .int()
18
20
  .positive()
21
+ .max(MAX_TASK_SCAN_LIMIT)
19
22
  .optional()
20
- .describe("Max failed tasks to scan (default 500)"),
23
+ .describe("Max failed tasks to scan (default 500, max 500)"),
21
24
  publishPage: z.boolean().optional().describe("Publish an authed HTML page (default true)"),
22
25
  });
23
26
 
@@ -25,20 +28,37 @@ const REASON_PATTERNS: any[] = [
25
28
  { key: "sigterm/killed", re: /sigterm|sigkill|killed|143|137/i },
26
29
  { key: "timeout", re: /time?d?\s*out|timeout|deadline/i },
27
30
  { key: "context-window", re: /context (window|limit|saturat)|peakcontext|compact/i },
31
+ { key: "reboot-sweep", re: /reboot sweep/i },
28
32
  { key: "not-found", re: /not found|404|missing|no such/i },
29
33
  { key: "auth/credentials", re: /unauthorized|401|403|credential|token|forbidden/i },
30
- { key: "ci/checks-failed", re: /ci|check.?s? fail|lint|tsc|test.?s? fail/i },
34
+ { key: "ci/checks-failed", re: /\bci\b|check.?s? fail|lint|tsc|test.?s? fail/i },
31
35
  { key: "network", re: /network|econn|fetch failed|socket|dns|502|503|504/i },
32
36
  { key: "cancelled", re: /cancel|aborted/i },
33
37
  ];
34
38
 
39
+ function sanitizeUntrustedReasonSample(reason: unknown): string {
40
+ return String(reason)
41
+ .replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
42
+ .replace(/\s+/g, " ")
43
+ .trim()
44
+ .slice(0, 200);
45
+ }
46
+
47
+ function rowsToObjects(result: any): any[] {
48
+ const payload = result?.data ?? result;
49
+ const columns: string[] = payload?.columns ?? [];
50
+ return (payload?.rows ?? []).map((row: any) =>
51
+ Array.isArray(row) ? Object.fromEntries(columns.map((column, i) => [column, row[i]])) : row,
52
+ );
53
+ }
54
+
35
55
  function reasonCluster(reason: string): string {
36
56
  const r = (reason || "").trim();
37
57
  if (!r) return "(no reason given)";
38
58
  for (const p of REASON_PATTERNS) {
39
59
  if (p.re.test(r)) return p.key;
40
60
  }
41
- return r.toLowerCase().slice(0, 48);
61
+ return "other";
42
62
  }
43
63
 
44
64
  /** Cluster recently failed swarm tasks by reason, agent, or schedule. */
@@ -47,7 +67,7 @@ export default async function taskFailureAudit(args: any, ctx: any) {
47
67
  if (!parsed.success) return { error: "invalid args: " + parsed.error.message };
48
68
  const days = parsed.data.days || 7;
49
69
  const groupBy = parsed.data.groupBy || "reason";
50
- const limit = parsed.data.limit || 500;
70
+ const limit = Math.min(parsed.data.limit ?? MAX_TASK_SCAN_LIMIT, MAX_TASK_SCAN_LIMIT);
51
71
  const publishPage = parsed.data.publishPage !== false;
52
72
 
53
73
  const since = new Date(Date.now() - days * 86400000).toISOString();
@@ -62,17 +82,49 @@ export default async function taskFailureAudit(args: any, ctx: any) {
62
82
  const payload: any = res && res.data ? res.data : res;
63
83
  const tasks: any = payload && Array.isArray(payload.tasks) ? payload.tasks : [];
64
84
 
85
+ const failureReasons = new Map<string, string>();
86
+ // Projected for every groupBy mode: task_list omits failureReason, and
87
+ // schedule/agent groups still need a populated sample.
88
+ const tasksNeedingReason = tasks.filter((task: any) => !task.failureReason && task.id);
89
+ const taskIds = tasksNeedingReason.map((task: any) => task.id as string);
90
+ if (taskIds.length > 0) {
91
+ const placeholders = taskIds.map(() => "?").join(", ");
92
+ const reasonResult: any = await ctx.swarm.db_query({
93
+ sql: `SELECT id, failureReason FROM agent_tasks WHERE id IN (${placeholders})`,
94
+ params: taskIds,
95
+ });
96
+ const reasonPayload: any = reasonResult?.data ?? reasonResult;
97
+ if (
98
+ reasonResult?.success === false ||
99
+ reasonPayload?.success === false ||
100
+ reasonPayload?.error
101
+ ) {
102
+ return { error: "failure reason projection failed with status " + reasonResult?.status };
103
+ }
104
+ if (reasonPayload?.truncated) {
105
+ return {
106
+ error: `failure reason projection truncated (${reasonPayload.rows?.length ?? 0} of ${reasonPayload.total ?? "unknown"} rows)`,
107
+ };
108
+ }
109
+ for (const row of rowsToObjects(reasonResult)) {
110
+ if (typeof row?.id === "string" && typeof row.failureReason === "string") {
111
+ failureReasons.set(row.id, row.failureReason);
112
+ }
113
+ }
114
+ }
115
+
65
116
  const groups: any = {};
66
117
  for (const t of tasks) {
67
118
  let key: string;
68
119
  if (groupBy === "agent") key = t.agentId || "(unassigned)";
69
120
  else if (groupBy === "schedule") key = t.scheduleId || "(not scheduled)";
70
- else key = reasonCluster(t.failureReason || "");
71
- if (!groups[key]) groups[key] = { key, count: 0, taskIds: [], sampleReason: "" };
121
+ else key = reasonCluster(failureReasons.get(t.id) || t.failureReason || "");
122
+ if (!groups[key]) groups[key] = { key, count: 0, taskIds: [], untrustedWorkerTextSample: "" };
72
123
  groups[key].count++;
73
124
  if (groups[key].taskIds.length < 5) groups[key].taskIds.push(t.id);
74
- if (!groups[key].sampleReason && t.failureReason) {
75
- groups[key].sampleReason = String(t.failureReason).slice(0, 200);
125
+ const failureReason = failureReasons.get(t.id) || t.failureReason;
126
+ if (!groups[key].untrustedWorkerTextSample && failureReason) {
127
+ groups[key].untrustedWorkerTextSample = sanitizeUntrustedReasonSample(failureReason);
76
128
  }
77
129
  }
78
130
 
@@ -118,7 +170,7 @@ export default async function taskFailureAudit(args: any, ctx: any) {
118
170
  key: group.key,
119
171
  count: group.count,
120
172
  taskIds: group.taskIds,
121
- sampleReason: group.sampleReason,
173
+ untrustedWorkerTextSample: group.untrustedWorkerTextSample,
122
174
  },
123
175
  ],
124
176
  })),
@@ -1,6 +1,7 @@
1
1
  import { normalizeSlackReactionShortcode } from "../slack/reaction-shortcode";
2
2
  import { ProviderNameSchema } from "../types";
3
3
  import { parseTaskToolManifest } from "../utils/task-tool-manifest";
4
+ import { isTierConfigKey, validateTierConfigValue } from "./model-tier-keys";
4
5
 
5
6
  /**
6
7
  * Guards against storing reserved keys in the swarm_config table.
@@ -274,6 +275,7 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
274
275
  return null;
275
276
  },
276
277
  ...booleanValidators([
278
+ "SWARM_DEV_MODE",
277
279
  "MULTI_RUNTIME_ENABLED",
278
280
  "STEERING_ENABLED",
279
281
  "MEMORY_HYBRID_SEARCH",
@@ -283,6 +285,8 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
283
285
  "POOL_AFFINITY_ENFORCEMENT",
284
286
  "SCRIPTS_ONLY_MCP",
285
287
  "TASK_TOOL_PRELOAD_ENABLED",
288
+ "PI_TOOL_DEFERRAL",
289
+ "PI_CODEMODE",
286
290
  "SLACK_DISABLE",
287
291
  "SLACK_RENDER_V2",
288
292
  "SLACK_RENDER_V2_DELEGATION",
@@ -304,6 +308,7 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
304
308
  "SWARM_HIDE_CLOUD_PROMO",
305
309
  "DB_QUERY_BOUNDED_ENABLED",
306
310
  "DB_RETENTION_DRY_RUN",
311
+ "MODEL_AUTO_UPGRADE",
307
312
  ]),
308
313
  ...enumValidator("SLACK_MODE", ["socket", "http"]),
309
314
  ...enumValidator("SLACK_THREAD_STEERING", ["off", "lead", "all"]),
@@ -359,6 +364,8 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
359
364
  ),
360
365
  // 0 is meaningful here: "auto-assign nothing this sweep".
361
366
  ...integerValidators(["HEARTBEAT_MAX_AUTO_ASSIGN"], 0),
367
+ // 0 disables the `latest:...@stable` soak window.
368
+ ...integerValidators(["MODEL_LATEST_SOAK_DAYS"], 0),
362
369
  // 0 turns approval auto-cancellation off.
363
370
  ...integerValidators(["APPROVAL_REQUEST_AUTO_CANCELLATION_DAYS"], 0),
364
371
  // Below ~100 tokens the preamble can't fit a useful summary; above 20000
@@ -378,6 +385,7 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
378
385
  };
379
386
 
380
387
  export function validateConfigValue(key: string, value: unknown): string | null {
388
+ if (isTierConfigKey(key)) return validateTierConfigValue(key.toUpperCase(), value);
381
389
  const validator = VALIDATED_KEYS[key.toUpperCase()];
382
390
  return validator ? validator(value) : null;
383
391
  }
package/src/cli.tsx CHANGED
@@ -717,6 +717,16 @@ if (args.showHelp || args.command === "help" || args.command === undefined) {
717
717
  } else if (args.command === "version") {
718
718
  console.log(`${binName} v${pkg.version}`);
719
719
  process.exit(0);
720
+ } else if (args.command === "onboard" && !process.versions.bun) {
721
+ // The published bin uses Node, but the onboarding wizard requires Bun APIs.
722
+ const { spawnSync } = await import("node:child_process");
723
+ const result = spawnSync("bun", process.argv.slice(1), { stdio: "inherit" });
724
+ if (result.error) {
725
+ console.error(
726
+ "Onboarding requires Bun. Install Bun from https://bun.sh, then retry this command.",
727
+ );
728
+ }
729
+ process.exit(result.status ?? 1);
720
730
  } else if (args.command === "docs") {
721
731
  const docsUrl = "https://docs.agent-swarm.dev";
722
732
  console.log(`\n${binName} docs — v${pkg.version}\n`);
@@ -34,6 +34,7 @@ import type { SkillCreateResponse } from "@anthropic-ai/sdk/resources/beta/skill
34
34
  import { toFile } from "@anthropic-ai/sdk/uploads";
35
35
 
36
36
  import { buildSkillContent, type SkillTemplateConfig } from "../be/seed-skills/render";
37
+ import { DEFAULT_MODEL_TIER_MAP } from "../types";
37
38
  import { getApiKey } from "../utils/api-key";
38
39
  import { promptHiddenInput } from "./codex-login.js";
39
40
 
@@ -65,7 +66,8 @@ export type ClaudeManagedSetupResult = {
65
66
 
66
67
  // ─── Constants ───────────────────────────────────────────────────────────────
67
68
 
68
- const DEFAULT_AGENT_MODEL = "claude-sonnet-5";
69
+ /** The claude-managed `regular` tier default — no per-model constant here. */
70
+ const DEFAULT_AGENT_MODEL = DEFAULT_MODEL_TIER_MAP["claude-managed"].regular;
69
71
  const SKILLS_DIR_RELATIVE = "plugin/commands";
70
72
  const SEEDED_SKILLS_DIR_RELATIVE = "templates/skills";
71
73
  const SKILLS_BETA_HEADER = "skills-2025-10-02";
@@ -12,6 +12,7 @@
12
12
  import { exec } from "node:child_process";
13
13
  import { emitKeypressEvents } from "node:readline";
14
14
 
15
+ import { deriveCodexKeySuffix } from "../providers/codex-oauth/auth-json.js";
15
16
  import { loginCodexOAuth } from "../providers/codex-oauth/flow.js";
16
17
  import {
17
18
  loadAllCodexOAuthSlots,
@@ -35,11 +36,59 @@ type RunCodexLoginDeps = {
35
36
  login?: typeof loginCodexOAuth;
36
37
  store?: typeof storeCodexOAuth;
37
38
  loadAllSlots?: typeof loadAllCodexOAuthSlots;
39
+ clearAuthBench?: typeof clearCodexAuthBench;
40
+ readAuthFence?: typeof readCodexAuthFailureFence;
38
41
  log?: (message: string) => void;
39
42
  error?: (message: string) => void;
40
43
  exit?: (code: number) => void;
41
44
  };
42
45
 
46
+ /**
47
+ * Read the API's `authFailureFence` for the Codex pool. `runCodexLogin` reads it
48
+ * BEFORE it stores the fresh credentials, so the clear below lifts only failures
49
+ * recorded before that write.
50
+ */
51
+ export async function readCodexAuthFailureFence(apiUrl: string, apiKey: string): Promise<number> {
52
+ const resp = await fetch(`${apiUrl}/api/keys/available?keyType=CODEX_OAUTH&totalKeys=1`, {
53
+ headers: { Authorization: `Bearer ${apiKey}` },
54
+ });
55
+ if (!resp.ok) throw new Error(`keys/available returned ${resp.status}`);
56
+ const data = (await resp.json()) as { authFailureFence?: number };
57
+ if (typeof data.authFailureFence !== "number") throw new Error("no authFailureFence in response");
58
+ return data.authFailureFence;
59
+ }
60
+
61
+ /**
62
+ * Lift an auth-failure bench on the re-logged key. A fresh login is proof of
63
+ * health, so it passes `clearAuthBench: true` (the API also deletes the
64
+ * `codex-auth-watch` bench marker). `authFence` binds the clear to the credential
65
+ * write: failures recorded after the fresh login survive a late clear. It also
66
+ * passes the slot, so the API retires a different login that the slot held
67
+ * before. Returns `true` when a bench was lifted.
68
+ */
69
+ export async function clearCodexAuthBench(
70
+ apiUrl: string,
71
+ apiKey: string,
72
+ keySuffix: string,
73
+ slot: number,
74
+ authFence: number,
75
+ ): Promise<boolean> {
76
+ const resp = await fetch(`${apiUrl}/api/keys/clear-rate-limit`, {
77
+ method: "POST",
78
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
79
+ body: JSON.stringify({
80
+ keyType: "CODEX_OAUTH",
81
+ keySuffix,
82
+ clearAuthBench: true,
83
+ keyIndex: slot,
84
+ authFence,
85
+ }),
86
+ });
87
+ if (!resp.ok) throw new Error(`clear-rate-limit returned ${resp.status}`);
88
+ const data = (await resp.json()) as { cleared?: boolean };
89
+ return data.cleared === true;
90
+ }
91
+
43
92
  type ParsedCodexLoginArgs = {
44
93
  apiUrl?: string;
45
94
  apiKey?: string;
@@ -207,6 +256,8 @@ export async function runCodexLogin(args: string[], deps: RunCodexLoginDeps = {}
207
256
  const login = deps.login ?? loginCodexOAuth;
208
257
  const store = deps.store ?? storeCodexOAuth;
209
258
  const loadAllSlots = deps.loadAllSlots ?? loadAllCodexOAuthSlots;
259
+ const clearAuthBench = deps.clearAuthBench ?? clearCodexAuthBench;
260
+ const readAuthFence = deps.readAuthFence ?? readCodexAuthFailureFence;
210
261
  const log = deps.log ?? console.log;
211
262
  const error = deps.error ?? console.error;
212
263
  const exit = deps.exit ?? ((code: number) => process.exit(code));
@@ -283,11 +334,27 @@ export async function runCodexLogin(args: string[], deps: RunCodexLoginDeps = {}
283
334
  log(` Account ID: ${creds.accountId}`);
284
335
  log(` Expires: ${new Date(creds.expires).toISOString()}`);
285
336
 
337
+ // Read the auth-failure fence before the credential write (see clearCodexAuthBench).
338
+ const authFence = await readAuthFence(apiUrl, apiKey).catch((err: unknown) => err);
339
+
286
340
  // Store credentials in the swarm API config store
287
341
  log("\nStoring credentials in swarm API config store...");
288
342
  await store(apiUrl, apiKey, creds, slot);
289
343
  log(`Credentials stored successfully in slot ${slot}!`);
290
344
 
345
+ // A re-login returns an auth-benched slot to the pool right away.
346
+ // Non-fatal: the credentials are stored; a later re-login also lifts it.
347
+ const keySuffix = deriveCodexKeySuffix(creds.access, creds.accountId);
348
+ try {
349
+ if (typeof authFence !== "number") throw authFence;
350
+ if (await clearAuthBench(apiUrl, apiKey, keySuffix, slot, authFence)) {
351
+ log(`Lifted the auth-failure bench on key ...${keySuffix}.`);
352
+ }
353
+ } catch (err) {
354
+ const message = err instanceof Error ? err.message : String(err);
355
+ error(`Warning: could not lift the auth-failure bench on key ...${keySuffix}: ${message}`);
356
+ }
357
+
291
358
  log("\nDeployed Codex workers will automatically restore these credentials at boot.");
292
359
  } catch (err) {
293
360
  const message = err instanceof Error ? err.message : String(err);
@@ -0,0 +1,191 @@
1
+ import type { RateLimitWindowTelemetry } from "../utils/error-tracker";
2
+ import { modelFamilyOf } from "../utils/model-rate-limit-windows";
3
+ import { buildFinalRateLimitWindows, classifyRateLimitOutcome } from "./rate-limit-outcome";
4
+ import { reportSeatMismatchOutcome } from "./seat-mismatch-report";
5
+
6
+ /** Report a rate-limited key to the API (fire-and-forget) */
7
+ async function reportKeyRateLimit(
8
+ apiUrl: string,
9
+ apiKey: string,
10
+ keyType: string,
11
+ keySuffix: string,
12
+ keyIndex: number,
13
+ rateLimitedUntil: string,
14
+ ): Promise<void> {
15
+ try {
16
+ await fetch(`${apiUrl}/api/keys/report-rate-limit`, {
17
+ method: "POST",
18
+ headers: {
19
+ "Content-Type": "application/json",
20
+ Authorization: `Bearer ${apiKey}`,
21
+ },
22
+ body: JSON.stringify({
23
+ keyType,
24
+ keySuffix,
25
+ keyIndex,
26
+ rateLimitedUntil,
27
+ }),
28
+ });
29
+ console.log(
30
+ `[credentials] Reported key ...${keySuffix} as rate-limited until ${rateLimitedUntil}`,
31
+ );
32
+ } catch {
33
+ // Non-blocking
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Reports rate-limit window telemetry for a key. Returns the underlying
39
+ * fetch promise (does not swallow errors) so a caller that needs the post to
40
+ * complete before the task finishes (a model-scoped block) can await it and
41
+ * decide how to handle a failure; a caller that wants the legacy
42
+ * fire-and-forget behavior appends `.catch(() => {})`.
43
+ *
44
+ * Throws on a non-2xx response so a failed persistence surfaces to the
45
+ * caller instead of logging success while only the in-process guard took
46
+ * effect — otherwise other workers redraw the same exhausted key at once.
47
+ *
48
+ * `logKeySuffix` defaults to true for the legacy full-telemetry call site;
49
+ * the model-scoped call site passes false since it already logs the model
50
+ * family and key index itself (see the `[credential] model window ...`
51
+ * log above the call).
52
+ */
53
+ export async function reportKeyRateLimitWindows(
54
+ apiUrl: string,
55
+ apiKey: string,
56
+ keyType: string,
57
+ keySuffix: string,
58
+ keyIndex: number,
59
+ windows: RateLimitWindowTelemetry,
60
+ logKeySuffix = true,
61
+ ): Promise<void> {
62
+ if (Object.keys(windows).length === 0) return;
63
+ const response = await fetch(`${apiUrl}/api/keys/report-rate-limit-windows`, {
64
+ method: "POST",
65
+ headers: {
66
+ "Content-Type": "application/json",
67
+ Authorization: `Bearer ${apiKey}`,
68
+ },
69
+ body: JSON.stringify({
70
+ keyType,
71
+ keySuffix,
72
+ keyIndex,
73
+ windows,
74
+ }),
75
+ });
76
+ if (!response.ok) {
77
+ throw new Error(
78
+ `Failed to report rate-limit windows for key #${keyIndex}: HTTP ${response.status}`,
79
+ );
80
+ }
81
+ if (logKeySuffix) {
82
+ console.log(`[credentials] Reported rate-limit windows for key ...${keySuffix}`);
83
+ }
84
+ }
85
+
86
+ export interface CredentialOutcomeInput {
87
+ apiUrl: string;
88
+ apiKey: string;
89
+ credentialInfo?: { keyType: string; keySuffix: string; keyIndex: number };
90
+ /** The finished session's `ProviderResult`. */
91
+ result: Parameters<typeof classifyRateLimitOutcome>[0];
92
+ failureReason: string | undefined;
93
+ model: string | undefined;
94
+ codexCreditsExhaustedCooldownMs: number;
95
+ /** Runner-local model window guard, keyed `keyType:keyIndex:window`. */
96
+ modelWindowBlocks: Map<string, number>;
97
+ }
98
+
99
+ /**
100
+ * Reports a finished session's credential outcome, then calls `finish`.
101
+ *
102
+ * If rate-limited and we know which key was used, report it. Codex adapter
103
+ * prefixes failure reasons with `[rate-limit]` / `[usage-limit]` (see
104
+ * codex-adapter.formatTerminalError); Claude surfaces "rate limit" / "hit
105
+ * your limit" via SessionErrorTracker.
106
+ *
107
+ * classifyRateLimitOutcome tests model-scoped windows (Fable/Opus/Sonnet
108
+ * weekly limits) before the legacy key-wide gate, so a model-scoped
109
+ * rejection blocks only that model family on this key — never the whole
110
+ * key — via report-rate-limit-windows instead of report-rate-limit. A
111
+ * key-wide rejection seen in the same session is still reported alongside
112
+ * it (windows are independent). The session's window telemetry and the
113
+ * classified model rejection go out as ONE payload, so an older `allowed`
114
+ * snapshot never overwrites the terminal rejection.
115
+ *
116
+ * Reports that gate admission on other workers (a seat mismatch, a model
117
+ * window rejection) land before `finish` runs. Plain telemetry and the
118
+ * key-wide report stay fire-and-forget.
119
+ */
120
+ export async function reportCredentialOutcomeThenFinish(
121
+ input: CredentialOutcomeInput,
122
+ finish: () => Promise<void>,
123
+ ): Promise<void> {
124
+ const { apiUrl, apiKey, credentialInfo, result, failureReason, model } = input;
125
+ if (credentialInfo) {
126
+ const outcome = classifyRateLimitOutcome(
127
+ result,
128
+ failureReason,
129
+ Date.now(),
130
+ input.codexCreditsExhaustedCooldownMs,
131
+ modelFamilyOf(model),
132
+ );
133
+ // A seat mismatch is not a rate limit: the key stays available for
134
+ // every model its seat can run. A seat outcome carries
135
+ // `keyRateLimitedUntil` only for an independent key-wide rejection seen
136
+ // earlier in the same session, which is still reported below.
137
+ if (outcome.kind === "seat") {
138
+ await reportSeatMismatchOutcome(apiUrl, apiKey, credentialInfo, outcome.model);
139
+ }
140
+ const keyRateLimitedUntil =
141
+ outcome.kind === "key"
142
+ ? outcome.rateLimitedUntil
143
+ : outcome.kind === "model" || outcome.kind === "seat"
144
+ ? outcome.keyRateLimitedUntil
145
+ : undefined;
146
+ if (keyRateLimitedUntil) {
147
+ console.log(`[credentials] Rate limit reset: ${keyRateLimitedUntil}`);
148
+ reportKeyRateLimit(
149
+ apiUrl,
150
+ apiKey,
151
+ credentialInfo.keyType,
152
+ credentialInfo.keySuffix,
153
+ credentialInfo.keyIndex,
154
+ keyRateLimitedUntil,
155
+ ).catch(() => {});
156
+ }
157
+ if (outcome.kind === "model") {
158
+ const resetsAtIso = new Date(outcome.resetsAtSec * 1000).toISOString();
159
+ const blockKey = `${credentialInfo.keyType}:${credentialInfo.keyIndex}:${outcome.window}`;
160
+ input.modelWindowBlocks.set(blockKey, outcome.resetsAtSec * 1000);
161
+ console.log(
162
+ `[credential] ${outcome.model} weekly window exhausted (${outcome.window}) on key #${credentialInfo.keyIndex} until ${resetsAtIso}`,
163
+ );
164
+ }
165
+
166
+ const finalWindows = buildFinalRateLimitWindows(
167
+ result.rateLimitWindows,
168
+ outcome,
169
+ new Date().toISOString(),
170
+ );
171
+ if (finalWindows) {
172
+ const report = reportKeyRateLimitWindows(
173
+ apiUrl,
174
+ apiKey,
175
+ credentialInfo.keyType,
176
+ credentialInfo.keySuffix,
177
+ credentialInfo.keyIndex,
178
+ finalWindows,
179
+ outcome.kind !== "model",
180
+ ).catch((err) => {
181
+ console.warn(
182
+ `[credential] Failed to report rate-limit windows: ${err instanceof Error ? err.message : String(err)}`,
183
+ );
184
+ });
185
+ // A model rejection gates admission on other workers: land it before
186
+ // the task finishes. Plain telemetry stays fire-and-forget.
187
+ if (outcome.kind === "model") await report;
188
+ }
189
+ }
190
+ await finish();
191
+ }
@@ -8,6 +8,7 @@ import {
8
8
  } from "../utils/error-tracker";
9
9
  import {
10
10
  type ModelFamily,
11
+ parseCreditsRequiredMessage,
11
12
  parseModelLimitMessage,
12
13
  windowForModelFamily,
13
14
  } from "../utils/model-rate-limit-windows";
@@ -15,6 +16,20 @@ import {
15
16
  export type RateLimitOutcome =
16
17
  | { kind: "none" }
17
18
  | { kind: "key"; rateLimitedUntil: string }
19
+ /**
20
+ * The key's seat cannot run the model (`credits_required`). Not a rate
21
+ * limit: the seat rejection never marks the key by itself.
22
+ */
23
+ | {
24
+ kind: "seat";
25
+ model: ModelFamily;
26
+ source: "event" | "text";
27
+ /**
28
+ * An independent key-wide rejection observed earlier in the same
29
+ * session, still to enforce. The seat outcome never erases it.
30
+ */
31
+ keyRateLimitedUntil?: string;
32
+ }
18
33
  | {
19
34
  kind: "model";
20
35
  model: ModelFamily;
@@ -38,6 +53,7 @@ interface ClassifiableResult {
38
53
  rateLimitResetAt?: string;
39
54
  rateLimitWindows?: RateLimitWindowTelemetry;
40
55
  modelRateLimit?: { window: string; model: ModelFamily; resetAt: string; observedAt?: string };
56
+ creditsRequired?: { observedAt: string; overageDisabledReason?: string };
41
57
  }
42
58
 
43
59
  function clampMs(candidateMs: number, nowMs: number): number {
@@ -48,10 +64,15 @@ function clampMs(candidateMs: number, nowMs: number): number {
48
64
 
49
65
  /**
50
66
  * Classifies a finished provider session's rate-limit signal into one of
51
- * three outcomes: no rate limit, a key-wide rate limit (legacy path), or a
52
- * model-scoped weekly-window rejection (Fable/Opus/Sonnet). Model outcomes
53
- * are tested before key outcomes so a model-scoped event or message never
54
- * falls into the legacy key-wide gate and marks the whole key.
67
+ * four outcomes: no rate limit, a key-wide rate limit (legacy path), a
68
+ * model-scoped weekly-window rejection (Fable/Opus/Sonnet), or a seat
69
+ * mismatch (`credits_required`: the seat cannot run the model). Seat
70
+ * outcomes are tested first and model outcomes before key outcomes, so
71
+ * neither falls into the legacy key-wide gate and marks the whole key.
72
+ *
73
+ * `taskModelFamily` is the family of the task's model. A structured
74
+ * `credits_required` event names no model, so without it (and without the
75
+ * failure text) the event cannot be attributed and is not a seat outcome.
55
76
  *
56
77
  * `codexCreditsExhaustedCooldownMs` defaults to the fixed constant so the
57
78
  * function stays pure and testable with 3 args; the runner call site passes
@@ -62,12 +83,21 @@ export function classifyRateLimitOutcome(
62
83
  failureReason: string | undefined,
63
84
  nowMs: number,
64
85
  codexCreditsExhaustedCooldownMs: number = CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS,
86
+ taskModelFamily?: ModelFamily,
65
87
  ): RateLimitOutcome {
66
88
  const keyRateLimitedUntil = result.rateLimitResetAt
67
89
  ? new Date(clampMs(new Date(result.rateLimitResetAt).getTime(), nowMs)).toISOString()
68
90
  : undefined;
69
91
  const keyExtra = keyRateLimitedUntil ? { keyRateLimitedUntil } : {};
70
92
 
93
+ if (failureReason != null) {
94
+ const seatFamily = parseCreditsRequiredMessage(failureReason);
95
+ if (seatFamily) return { kind: "seat", model: seatFamily, source: "text", ...keyExtra };
96
+ }
97
+ if (result.creditsRequired && taskModelFamily) {
98
+ return { kind: "seat", model: taskModelFamily, source: "event", ...keyExtra };
99
+ }
100
+
71
101
  if (result.modelRateLimit) {
72
102
  const resetsAtSec = Math.floor(new Date(result.modelRateLimit.resetAt).getTime() / 1000);
73
103
  const { observedAt } = result.modelRateLimit;