@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
@@ -4,7 +4,10 @@
4
4
  * This module is safe for both API and worker code — it has NO database imports.
5
5
  *
6
6
  * Phase 4 + Phase 9 of the cost-tracking plan:
7
- * - `getContextWindowSize` now resolves shortnames, family-versioned ids
7
+ * - Model-catalog phase 4: windows come from the model catalog (no
8
+ * per-model table). A new model gets the right window as soon as it
9
+ * lands in `model_catalog`.
10
+ * - `getContextWindowSize` resolves shortnames, family-versioned ids
8
11
  * (`claude-sonnet-4-6`), AND dated full ids (`claude-sonnet-4-6-20251004`)
9
12
  * by stripping the trailing date suffix. Previously the dated form fell
10
13
  * to the 200k default — wildly wrong for sonnet/opus 4.x.
@@ -17,6 +20,9 @@
17
20
  * deprecated; new code should use `computeContextUsedUnified`.
18
21
  */
19
22
 
23
+ import { buildClaudeShortnameMap } from "@desplega/model-catalog";
24
+ import { runtimeCatalogModel, runtimeCatalogSection } from "./runtime-model-catalog";
25
+
20
26
  /**
21
27
  * Phase 9: stamp this onto every `context_usage` event the adapter emits.
22
28
  * Callers that compute their own number for legacy reasons (e.g. pi-mono
@@ -25,43 +31,6 @@
25
31
  */
26
32
  export const CONTEXT_FORMULA = "input-cache-output" as const;
27
33
 
28
- const CONTEXT_WINDOW_DEFAULTS: Record<string, number> = {
29
- // Anthropic Fable / Mythos tier
30
- "claude-fable-5-1": 1_000_000,
31
- "claude-mythos-5-1": 1_000_000,
32
- "claude-opus-5-5": 1_000_000,
33
- "claude-opus-5": 1_000_000,
34
- "claude-fable-5": 1_000_000,
35
- "claude-mythos-5": 1_000_000,
36
- "claude-sonnet-5-5": 1_000_000,
37
- "claude-sonnet-5": 1_000_000,
38
- // Anthropic 4.x family
39
- "claude-opus-4-8": 1_000_000,
40
- "claude-opus-4-7": 1_000_000,
41
- "claude-opus-4-6": 1_000_000,
42
- "claude-opus-4-5": 1_000_000,
43
- "claude-opus-4-1": 200_000,
44
- "claude-opus-4-0": 200_000,
45
- "claude-sonnet-4-6": 1_000_000,
46
- "claude-sonnet-4-5": 1_000_000,
47
- "claude-sonnet-4-0": 200_000,
48
- "claude-haiku-4-5": 200_000,
49
- // Anthropic 3.x family (legacy)
50
- "claude-3-7-sonnet": 200_000,
51
- "claude-3-5-sonnet": 200_000,
52
- "claude-3-5-haiku": 200_000,
53
- "claude-3-opus": 200_000,
54
- "claude-3-sonnet": 200_000,
55
- "claude-3-haiku": 200_000,
56
- // Shortnames used by the local-CLI adapter and pi-mono OpenRouter mirror.
57
- fable: 1_000_000,
58
- mythos: 1_000_000,
59
- opus: 1_000_000,
60
- sonnet: 1_000_000,
61
- haiku: 200_000,
62
- default: 200_000,
63
- };
64
-
65
34
  const DEFAULT_CONTEXT_WINDOW = 200_000;
66
35
 
67
36
  /**
@@ -69,30 +38,55 @@ const DEFAULT_CONTEXT_WINDOW = 200_000;
69
38
  * resolve to the same window as the family-versioned id.
70
39
  *
71
40
  * `claude-sonnet-4-6-20251004` → `claude-sonnet-4-6`
72
- * `claude-haiku-4-5-20251001` → `claude-haiku-4-5`
73
- *
74
- * Anthropic's dated full ids are always `${family}-${major}-${minor}-${YYYYMMDD}`,
75
- * so an 8-digit trailing date is a reliable signal.
76
41
  */
77
42
  function stripAnthropicDateSuffix(model: string): string {
78
43
  return model.replace(/-(\d{8})$/, "");
79
44
  }
80
45
 
46
+ let shortnameMap: { section: Record<string, unknown>; map: Record<string, string> } | null = null;
47
+
48
+ /** Claude CLI shortnames (`opus`, `sonnet`, ...) → newest catalog id. */
49
+ function resolveClaudeShortname(model: string): string | undefined {
50
+ const section = runtimeCatalogSection("anthropic");
51
+ if (!shortnameMap || shortnameMap.section !== section) {
52
+ shortnameMap = { section, map: buildClaudeShortnameMap(section) };
53
+ }
54
+ return shortnameMap.map[model.trim().toLowerCase()];
55
+ }
56
+
57
+ function catalogContext(provider: string, model: string): number | undefined {
58
+ const context = runtimeCatalogModel(provider, model)?.limit?.context;
59
+ return typeof context === "number" && context > 0 ? context : undefined;
60
+ }
61
+
62
+ /**
63
+ * Context window (tokens) for a model id, read from the model catalog
64
+ * (`src/utils/runtime-model-catalog.ts`: live `model_catalog` + overlay,
65
+ * vendored models.dev snapshot offline). Accepts Anthropic ids (dated or
66
+ * not), Claude CLI shortnames, OpenAI/Codex ids and `provider/model` ids.
67
+ * Unknown models get a conservative 200k so percent math stays finite.
68
+ */
81
69
  export function getContextWindowSize(model: string): number {
82
- // Fast path: exact match (shortname or family-versioned id).
83
- if (CONTEXT_WINDOW_DEFAULTS[model] !== undefined) {
84
- return CONTEXT_WINDOW_DEFAULTS[model];
70
+ if (!model) return DEFAULT_CONTEXT_WINDOW;
71
+ const slash = model.indexOf("/");
72
+ if (slash > 0) {
73
+ const scoped = catalogContext(model.slice(0, slash), model.slice(slash + 1));
74
+ if (scoped) return scoped;
75
+ }
76
+ for (const provider of ["anthropic", "openai"]) {
77
+ const exact = catalogContext(provider, model);
78
+ if (exact) return exact;
85
79
  }
86
- // Dated full id → strip suffix and retry.
87
80
  const stripped = stripAnthropicDateSuffix(model);
88
- if (stripped !== model && CONTEXT_WINDOW_DEFAULTS[stripped] !== undefined) {
89
- return CONTEXT_WINDOW_DEFAULTS[stripped];
81
+ if (stripped !== model) {
82
+ const dated = catalogContext("anthropic", stripped);
83
+ if (dated) return dated;
84
+ }
85
+ const shortname = resolveClaudeShortname(model);
86
+ if (shortname) {
87
+ const resolved = catalogContext("anthropic", shortname);
88
+ if (resolved) return resolved;
90
89
  }
91
- // OpenAI / GPT family — most reasoning models have 200k+; we keep this
92
- // conservative and let callers override via models.dev rates if they want.
93
- // Specific gpt-5.x context windows are >1M but the local-CLI adapter
94
- // generally doesn't surface those; the API recompute path uses the rate
95
- // table, not the window. The 200k default keeps the math safe.
96
90
  return DEFAULT_CONTEXT_WINDOW;
97
91
  }
98
92
 
@@ -113,8 +113,12 @@ export interface CredentialSelection {
113
113
  modelBlockedIndices?: number[];
114
114
  /** ISO of the earliest reset among modelBlockedIndices, or null/undefined when none. */
115
115
  earliestModelResetAt?: string | null;
116
+ /** Indices excluded because the key's subscription plan cannot run the requested model. */
117
+ seatBlockedIndices?: number[];
116
118
  /** Subscription plan id detected on the credential (`SUBSCRIPTION_PLANS`), reported with its usage. */
117
119
  plan?: string | null;
120
+ /** `authFailureFence` from `GET /api/keys/available`, read before the task (Codex pool only). */
121
+ authFailureFence?: number;
118
122
  }
119
123
 
120
124
  const MODEL_LABELS: Record<ModelFamily, string> = {
@@ -126,33 +130,49 @@ const MODEL_LABELS: Record<ModelFamily, string> = {
126
130
 
127
131
  /**
128
132
  * Thrown by `resolveCredentialPools` at task admission (`enforceModelCapacity`)
129
- * when every key for a pool is either key-wide rate-limited or blocked by the
130
- * requested model's weekly window, and `MODEL_WINDOW_EXHAUSTED_POLICY` is
131
- * `fail` (the default). The caller must not spawn the CLI on this error — see
132
- * `spawnProviderProcess` in `src/commands/runner.ts`. Taskless configuration
133
- * loads never throw it.
133
+ * when no key for a pool can run the requested model: every key is key-wide
134
+ * rate-limited, blocked by the model's weekly window, or on a subscription
135
+ * seat that cannot run the model (`seatBlockedCount`). A window block throws
136
+ * only when `MODEL_WINDOW_EXHAUSTED_POLICY` is `fail` (the default); a seat
137
+ * block never resets, so it throws under every policy. The caller must not
138
+ * spawn the CLI on this error — see `spawnProviderProcess` in
139
+ * `src/commands/runner.ts`. Taskless configuration loads never throw it.
134
140
  */
135
141
  export class ModelWindowExhaustedError extends Error {
136
142
  readonly model: ModelFamily;
137
143
  readonly window: string;
138
144
  readonly earliestResetAt: string | null;
139
145
  readonly keyType: string;
146
+ readonly seatBlockedCount: number;
147
+ readonly modelBlockedCount: number;
140
148
 
141
149
  constructor(opts: {
142
150
  model: ModelFamily;
143
151
  window: string;
144
152
  earliestResetAt: string | null;
145
153
  keyType: string;
154
+ seatBlockedCount?: number;
155
+ modelBlockedCount?: number;
146
156
  }) {
147
157
  const modelLabel = MODEL_LABELS[opts.model];
158
+ const seatBlockedCount = opts.seatBlockedCount ?? 0;
159
+ const modelBlockedCount = opts.modelBlockedCount ?? 0;
160
+ const windowClause =
161
+ modelBlockedCount > 0 && opts.earliestResetAt
162
+ ? `${modelBlockedCount} keys have the ${modelLabel} window exhausted until ${opts.earliestResetAt}`
163
+ : `${modelBlockedCount} keys have the ${modelLabel} window exhausted`;
148
164
  super(
149
- `No ${opts.keyType} key has ${modelLabel} capacity until ${opts.earliestResetAt ?? "unknown"}. Re-dispatch with another model or modelTier.`,
165
+ seatBlockedCount > 0
166
+ ? `No ${opts.keyType} key can run ${modelLabel}: ${seatBlockedCount} keys are on a seat without ${modelLabel}, ${windowClause}. Re-dispatch with another model or modelTier.`
167
+ : `No ${opts.keyType} key has ${modelLabel} capacity until ${opts.earliestResetAt ?? "unknown"}. Re-dispatch with another model or modelTier.`,
150
168
  );
151
169
  this.name = "ModelWindowExhaustedError";
152
170
  this.model = opts.model;
153
171
  this.window = opts.window;
154
172
  this.earliestResetAt = opts.earliestResetAt;
155
173
  this.keyType = opts.keyType;
174
+ this.seatBlockedCount = seatBlockedCount;
175
+ this.modelBlockedCount = modelBlockedCount;
156
176
  }
157
177
  }
158
178
 
@@ -173,6 +193,15 @@ function isJsonObject(value: string): boolean {
173
193
  * Falls back to random selection from all credentials if no available indices match.
174
194
  * A JSON object value (the CODEX_OAUTH blob) is always one credential.
175
195
  */
196
+ function splitCredentialPool(value: string): string[] {
197
+ return isJsonObject(value)
198
+ ? [value.trim()]
199
+ : value
200
+ .split(",")
201
+ .map((s) => s.trim())
202
+ .filter(Boolean);
203
+ }
204
+
176
205
  export function selectCredential(
177
206
  value: string,
178
207
  availableIndices?: number[],
@@ -183,12 +212,7 @@ export function selectCredential(
183
212
  // handed out fragments such as `"accountId":"..."}` as the selected credential
184
213
  // and stamped `965"}` as the key suffix on task records. Codex pools live in
185
214
  // config-store slots (codex_oauth_<n>), resolved by the runner instead.
186
- const credentials = isJsonObject(value)
187
- ? [value.trim()]
188
- : value
189
- .split(",")
190
- .map((s) => s.trim())
191
- .filter(Boolean);
215
+ const credentials = splitCredentialPool(value);
192
216
  if (credentials.length <= 1) {
193
217
  const selected = value.trim();
194
218
  const isRateLimitFallback = availableIndices !== undefined && availableIndices.length === 0;
@@ -283,6 +307,8 @@ export interface AvailabilityInfo {
283
307
  availableIndices: number[];
284
308
  modelBlockedIndices?: number[];
285
309
  earliestModelResetAt?: string | null;
310
+ /** Indices excluded because the key's subscription plan cannot run the model. */
311
+ seatBlockedIndices?: number[];
286
312
  }
287
313
 
288
314
  /**
@@ -315,11 +341,13 @@ async function fetchAvailableIndices(
315
341
  availableIndices: number[];
316
342
  modelBlockedIndices?: number[];
317
343
  earliestModelResetAt?: string | null;
344
+ seatBlockedIndices?: number[];
318
345
  };
319
346
  availableIndicesMap[envVar] = {
320
347
  availableIndices: data.availableIndices,
321
348
  modelBlockedIndices: data.modelBlockedIndices,
322
349
  earliestModelResetAt: data.earliestModelResetAt,
350
+ seatBlockedIndices: data.seatBlockedIndices,
323
351
  };
324
352
  if (data.availableIndices.length < totalKeys) {
325
353
  console.log(
@@ -374,7 +402,9 @@ export async function resolveCredentialPools(
374
402
  localBlocks?: Map<string, number>;
375
403
  /**
376
404
  * Set only at task admission (`spawnProviderProcess`). When true, an
377
- * exhausted model window fails fast with `ModelWindowExhaustedError`.
405
+ * exhausted model window, or a pool where every remaining key is on a
406
+ * seat that cannot run the model, fails fast with
407
+ * `ModelWindowExhaustedError`.
378
408
  * Taskless configuration loads (worker boot, credential recovery,
379
409
  * periodic reconciliation) leave it unset: they still pick a key, so an
380
410
  * exhausted default model never blocks boot or config refresh, and the
@@ -413,32 +443,43 @@ export async function resolveCredentialPools(
413
443
  modelBlockedCount += beforeCount - available.length;
414
444
  }
415
445
 
416
- // Every key is either key-wide rate-limited or blocked by this
417
- // model's weekly window, and at least one is blocked specifically by
418
- // the model (not just legacy key-wide rate limiting) — the picker
419
- // can't make progress for this model on this pool. Default policy
420
- // fails fast instead of looping the worker through the same
421
- // exhausted key every few minutes.
422
- if (
423
- opts?.enforceModelCapacity &&
424
- window &&
425
- modelFamily &&
426
- available &&
427
- available.length === 0 &&
428
- modelBlockedCount > 0
429
- ) {
446
+ const seatBlockedIndices = info?.seatBlockedIndices ?? [];
447
+ const seatBlockedCount = seatBlockedIndices.length;
448
+ let seatFallbackIndices: number[] | undefined;
449
+
450
+ // Every key is key-wide rate-limited, blocked by this model's weekly
451
+ // window, or on a seat that cannot run the model, and at least one is
452
+ // blocked specifically for the model (not just legacy key-wide rate
453
+ // limiting) — the picker can't make progress for this model on this
454
+ // pool. Default policy fails fast instead of looping the worker
455
+ // through the same exhausted key every few minutes. A seat block is a
456
+ // plan fact with no window and no reset, so the fallback pick excludes
457
+ // seat-blocked keys: a pick from them is a guaranteed failure. It fails
458
+ // only when every key is seat-blocked.
459
+ if (opts?.enforceModelCapacity && modelFamily && available && available.length === 0) {
430
460
  const policy = (env.MODEL_WINDOW_EXHAUSTED_POLICY ?? "fail").trim().toLowerCase();
431
- if (policy !== "fallback") {
461
+ const windowExhausted =
462
+ window !== undefined && modelBlockedCount > 0 && policy !== "fallback";
463
+ if (seatBlockedCount > 0) {
464
+ seatFallbackIndices = splitCredentialPool(val)
465
+ .map((_, i) => i)
466
+ .filter((i) => !seatBlockedIndices.includes(i));
467
+ }
468
+ if (windowExhausted || seatFallbackIndices?.length === 0) {
432
469
  throw new ModelWindowExhaustedError({
433
470
  model: modelFamily,
434
- window,
471
+ window: window ?? "",
435
472
  earliestResetAt: info?.earliestModelResetAt ?? null,
436
473
  keyType: envVar,
474
+ seatBlockedCount,
475
+ modelBlockedCount,
437
476
  });
438
477
  }
439
478
  }
440
479
 
441
- const result = selectCredential(val, available, envVar);
480
+ const result = seatFallbackIndices
481
+ ? { ...selectCredential(val, seatFallbackIndices, envVar), isRateLimitFallback: true }
482
+ : selectCredential(val, available, envVar);
442
483
  env[envVar] = result.selected;
443
484
  const availInfo = available ? ` (${available.length} available of ${result.total})` : "";
444
485
  console.log(
@@ -448,6 +489,7 @@ export async function resolveCredentialPools(
448
489
  ...result,
449
490
  modelBlockedIndices: info?.modelBlockedIndices,
450
491
  earliestModelResetAt: info?.earliestModelResetAt,
492
+ seatBlockedIndices: info?.seatBlockedIndices,
451
493
  });
452
494
  }
453
495
  }
@@ -95,6 +95,51 @@ export function parseRateLimitWindowTelemetry(
95
95
  }
96
96
  }
97
97
 
98
+ /**
99
+ * `rate_limit_info` fields that describe the seat's billing state (the
100
+ * `credits_required` payload). `scrubSecrets` does not match them, so they
101
+ * are dropped structurally before a log egress.
102
+ */
103
+ const SEAT_PAYLOAD_FIELDS = [
104
+ "overageDisabledReason",
105
+ "canUserPurchaseCredits",
106
+ "hasChargeableSavedPaymentMethod",
107
+ ] as const;
108
+
109
+ /**
110
+ * Returns a `rate_limit_event` message without the seat billing fields of
111
+ * its `rate_limit_info`. Any other message is returned unchanged. Error
112
+ * tracking reads the original message; only log egress uses the result.
113
+ */
114
+ export function redactRateLimitEvent<T>(message: T): T {
115
+ if (!message || typeof message !== "object") return message;
116
+ const json = message as Record<string, unknown>;
117
+ if (json.type !== "rate_limit_event") return message;
118
+ const info = json.rate_limit_info;
119
+ if (!info || typeof info !== "object") return message;
120
+ if (!SEAT_PAYLOAD_FIELDS.some((field) => field in info)) return message;
121
+ const redactedInfo = { ...(info as Record<string, unknown>) };
122
+ for (const field of SEAT_PAYLOAD_FIELDS) delete redactedInfo[field];
123
+ return { ...json, rate_limit_info: redactedInfo } as T;
124
+ }
125
+
126
+ /**
127
+ * Line form of `redactRateLimitEvent` for the CLI's stream-json output. A
128
+ * line that is not a `rate_limit_event` with seat billing fields is returned
129
+ * byte for byte.
130
+ */
131
+ export function redactRateLimitEventLine(line: string): string {
132
+ if (!line.includes('"rate_limit_event"')) return line;
133
+ let parsed: unknown;
134
+ try {
135
+ parsed = JSON.parse(line);
136
+ } catch {
137
+ return line;
138
+ }
139
+ const redacted = redactRateLimitEvent(parsed);
140
+ return redacted === parsed ? line : JSON.stringify(redacted);
141
+ }
142
+
98
143
  /**
99
144
  * Maximum cooldown horizon for a rate-limit reset. A weekly OAuth limit resets
100
145
  * up to ~7 days out, so the cap must be at least that or a weekly-limited key
@@ -186,6 +231,14 @@ export class SessionErrorTracker {
186
231
  private modelRateLimit:
187
232
  | { window: string; model: ModelFamily; resetAtMs: number; observedAt: string }
188
233
  | undefined;
234
+ /**
235
+ * Stashed `credits_required` rejection: the key's seat cannot run the
236
+ * session's model (e.g. "Fable 5.1 requires usage credits"). A seat fact,
237
+ * not a rate limit, so it never lands in `keyWideRejections`.
238
+ */
239
+ private creditsRequired:
240
+ | { observedAt: string; resetsAtMs?: number; overageDisabledReason?: string }
241
+ | undefined;
189
242
  private rateLimitWindows: RateLimitWindowTelemetry = {};
190
243
 
191
244
  /** Record an error from an assistant message with message.error field */
@@ -265,6 +318,21 @@ export class SessionErrorTracker {
265
318
  return;
266
319
  }
267
320
 
321
+ // Checked before the resetsAt validation: the observed event carries a
322
+ // resetsAt, but this branch must not depend on it.
323
+ if (info.errorCode === "credits_required") {
324
+ this.creditsRequired = {
325
+ observedAt,
326
+ resetsAtMs:
327
+ typeof info.resetsAt === "number" && Number.isFinite(info.resetsAt)
328
+ ? info.resetsAt * 1000
329
+ : undefined,
330
+ overageDisabledReason:
331
+ typeof info.overageDisabledReason === "string" ? info.overageDisabledReason : undefined,
332
+ };
333
+ return;
334
+ }
335
+
268
336
  const resetsAtSec = info.resetsAt;
269
337
  if (typeof resetsAtSec !== "number" || !Number.isFinite(resetsAtSec) || resetsAtSec <= 0) {
270
338
  console.warn(
@@ -344,6 +412,18 @@ export class SessionErrorTracker {
344
412
  };
345
413
  }
346
414
 
415
+ /**
416
+ * Returns the stashed `credits_required` rejection, or undefined if none was
417
+ * seen in this session. It never sets the key-wide reset time.
418
+ */
419
+ getCreditsRequired(): { observedAt: string; overageDisabledReason?: string } | undefined {
420
+ if (!this.creditsRequired) return undefined;
421
+ return {
422
+ observedAt: this.creditsRequired.observedAt,
423
+ overageDisabledReason: this.creditsRequired.overageDisabledReason,
424
+ };
425
+ }
426
+
347
427
  hasErrors(): boolean {
348
428
  return this.errors.length > 0;
349
429
  }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The worker's harness CLI version (`claude --version` / `codex --version`),
3
+ * reported on register and used to key harness_model_support rows. Worker-safe
4
+ * (no DB). Probed once per process; null when the CLI is missing or silent.
5
+ */
6
+ import { isUnknownModelError } from "./harness-model-error";
7
+ import { scrubSecrets } from "./secret-scrubber";
8
+
9
+ const CLI_BINARY: Record<string, string> = { claude: "claude", codex: "codex" };
10
+ const PROBE_TIMEOUT_MS = 10_000;
11
+ const VERSION_RE = /\d+\.\d+\.\d+(?:[-+][\w.]+)?/;
12
+
13
+ const probed = new Map<string, Promise<string | null>>();
14
+
15
+ export function parseCliVersion(output: string): string | null {
16
+ return VERSION_RE.exec(output)?.[0] ?? null;
17
+ }
18
+
19
+ export function probeHarnessCliVersion(harness: string): Promise<string | null> {
20
+ const binary = CLI_BINARY[harness];
21
+ if (!binary) return Promise.resolve(null);
22
+ let pending = probed.get(harness);
23
+ if (!pending) {
24
+ pending = (async () => {
25
+ try {
26
+ const proc = Bun.spawn([binary, "--version"], {
27
+ stdin: "ignore",
28
+ stdout: "pipe",
29
+ stderr: "pipe",
30
+ timeout: PROBE_TIMEOUT_MS,
31
+ killSignal: "SIGKILL",
32
+ });
33
+ const [stdout, exitCode] = await Promise.all([
34
+ new Response(proc.stdout).text(),
35
+ proc.exited,
36
+ ]);
37
+ return exitCode === 0 ? parseCliVersion(stdout) : null;
38
+ } catch {
39
+ return null;
40
+ }
41
+ })();
42
+ probed.set(harness, pending);
43
+ }
44
+ return pending;
45
+ }
46
+
47
+ /** Last probed version, without waiting; null until the probe finished. */
48
+ export async function cachedHarnessCliVersion(harness: string): Promise<string | null> {
49
+ return (await probed.get(harness)) ?? null;
50
+ }
51
+
52
+ const reported = new Set<string>();
53
+
54
+ /**
55
+ * Record a model's run outcome on this worker's CLI. `ok` is sent once per
56
+ * model per process; `unsupported` whenever the CLI rejects the id. Other
57
+ * failures say nothing about support and are not sent. Never throws.
58
+ */
59
+ export async function reportHarnessModelOutcome(opts: {
60
+ apiUrl: string;
61
+ apiKey?: string;
62
+ agentId: string;
63
+ harness: string;
64
+ model: string | undefined;
65
+ exitCode: number;
66
+ failureReason?: string;
67
+ fetchImpl?: typeof fetch;
68
+ }): Promise<void> {
69
+ if (!opts.model || !CLI_BINARY[opts.harness]) return;
70
+ const cliVersion = await cachedHarnessCliVersion(opts.harness);
71
+ if (!cliVersion) return;
72
+ const status =
73
+ opts.exitCode === 0
74
+ ? "ok"
75
+ : isUnknownModelError(opts.failureReason, opts.model)
76
+ ? "unsupported"
77
+ : null;
78
+ if (!status) return;
79
+ const key = `${opts.harness}|${cliVersion}|${opts.model}`;
80
+ if (status === "ok" && reported.has(key)) return;
81
+ try {
82
+ const headers: Record<string, string> = {
83
+ "Content-Type": "application/json",
84
+ "X-Agent-ID": opts.agentId,
85
+ };
86
+ if (opts.apiKey) headers.Authorization = `Bearer ${opts.apiKey}`;
87
+ const res = await (opts.fetchImpl ?? fetch)(
88
+ `${opts.apiUrl}/api/models-catalog/harness-support`,
89
+ {
90
+ method: "PUT",
91
+ headers,
92
+ body: JSON.stringify({
93
+ harness: opts.harness,
94
+ cliVersion,
95
+ modelId: opts.model,
96
+ status,
97
+ ...(status === "unsupported" && opts.failureReason
98
+ ? { error: scrubSecrets(opts.failureReason).slice(0, 2000) }
99
+ : {}),
100
+ }),
101
+ },
102
+ );
103
+ if (res.ok && status === "ok") reported.add(key);
104
+ } catch {
105
+ // Best effort: the next run reports again.
106
+ }
107
+ }
108
+
109
+ export function resetHarnessCliVersionForTests(seed?: Record<string, string | null>): void {
110
+ probed.clear();
111
+ reported.clear();
112
+ for (const [harness, version] of Object.entries(seed ?? {})) {
113
+ probed.set(harness, Promise.resolve(version));
114
+ }
115
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Recognizes a harness CLI rejecting a model id. Worker-safe (no DB).
3
+ *
4
+ * Claude Code: "There's an issue with the selected model (x). It may not exist
5
+ * or you may not have access to it", or the API's `not_found_error` "model: x".
6
+ * Codex: "The model `x` does not exist or you do not have access to it",
7
+ * "model is not supported", "unknown model".
8
+ *
9
+ * A model-rejection phrase can also appear in an unrelated failure ("invalid model output"),
10
+ * and a false positive marks the model unsupported for the whole (harness, CLI version). So
11
+ * when `modelId` is given, the rejection line must name that model.
12
+ */
13
+ const UNKNOWN_MODEL_PATTERNS: RegExp[] = [
14
+ /issue with the selected model/i,
15
+ /model[`'" ]*[\w.:/-]*[`'" ]*(?:does not exist|not found)/i,
16
+ /not_found_error[^\n]*model/i,
17
+ /model[^\n]{0,80}is not supported/i,
18
+ /unsupported model/i,
19
+ /unknown model/i,
20
+ /invalid model/i,
21
+ ];
22
+
23
+ export function isUnknownModelError(
24
+ text: string | null | undefined,
25
+ modelId?: string | null,
26
+ ): boolean {
27
+ if (!text) return false;
28
+ const id = modelId?.trim().toLowerCase();
29
+ return text.split("\n").some((line) => {
30
+ if (!UNKNOWN_MODEL_PATTERNS.some((re) => re.test(line))) return false;
31
+ return !id || line.toLowerCase().includes(id);
32
+ });
33
+ }
@@ -73,6 +73,18 @@ export function parseModelLimitMessage(text: string): ModelFamily | undefined {
73
73
  return match[1]!.toLowerCase() as ModelFamily;
74
74
  }
75
75
 
76
+ /**
77
+ * Text signal for a seat that cannot run the model. Matches the CLI's failure
78
+ * text for a `credits_required` rejection, e.g. "Fable 5.1 requires usage
79
+ * credits. Switch to another model to continue." A dot followed by a digit
80
+ * is allowed so a version number ("5.1") does not end the match.
81
+ */
82
+ export function parseCreditsRequiredMessage(text: string): ModelFamily | undefined {
83
+ const match = text.match(/(Fable|Opus|Sonnet)(?:[^.]|\.(?=\d)){0,40}requires usage credits/i);
84
+ if (!match) return undefined;
85
+ return match[1]!.toLowerCase() as ModelFamily;
86
+ }
87
+
76
88
  interface RateLimitWindowLike {
77
89
  status: string;
78
90
  resetsAt?: number;