@desplega.ai/agent-swarm 1.158.0 → 1.159.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/dist/{acp-adapter-xaq4n6pc.js → acp-adapter-qhch0n2v.js} +1 -1
  2. package/dist/{actions-p9hz5yat.js → actions-dtsh1mfb.js} +33 -29
  3. package/dist/{anthropic-messages-51pz26qe.js → anthropic-messages-f2h06k64.js} +8 -3
  4. package/dist/{app-g26wdcz6.js → app-y1b9srva.js} +10 -9
  5. package/dist/{assistant-wydfqkb3.js → assistant-31hwy5xj.js} +34 -30
  6. package/dist/{azure-openai-responses-8gksqtmz.js → azure-openai-responses-0k1mfax5.js} +12 -13
  7. package/dist/{boot-reembed-se8zmdcp.js → boot-reembed-48d2qpkt.js} +11 -11
  8. package/dist/{boot-reembed-g5ckg9yb.js → boot-reembed-w0bryhkr.js} +12 -12
  9. package/dist/{boot-scrub-logs-wfxqan61.js → boot-scrub-logs-t65wbrn7.js} +9 -8
  10. package/dist/{boot-seeds-s3q01xj3.js → boot-seeds-h7c32hvm.js} +12 -11
  11. package/dist/{claude-adapter-tv2x4f36.js → claude-adapter-1t6611ne.js} +26 -21
  12. package/dist/{claude-managed-adapter-41b2s7fd.js → claude-managed-adapter-werp659j.js} +4 -2
  13. package/dist/{claude-managed-setup-60g8aazx.js → claude-managed-setup-y6rdgbc1.js} +11 -2
  14. package/dist/claude-sdk-session-zhkyk8yw.js +29319 -0
  15. package/dist/{cli-vj6h9kc2.js → cli-0m7wct0d.js} +59 -1
  16. package/dist/{cli-wscdvcqz.js → cli-0n2e4x49.js} +1 -1
  17. package/dist/cli-0xqde53b.js +176 -0
  18. package/dist/{cli-xberxa1r.js → cli-10ym38qc.js} +2 -2
  19. package/dist/{cli-5b6b62tz.js → cli-13hw4z91.js} +1 -1
  20. package/dist/{cli-rqz8qbw7.js → cli-20035trh.js} +50 -18
  21. package/dist/{cli-k0g1sjzm.js → cli-31nkw3g3.js} +3 -3
  22. package/dist/cli-3gvrr8vj.js +1422 -0
  23. package/dist/{cli-c07r9a2m.js → cli-48527fpw.js} +9 -8
  24. package/dist/{cli-24k806sc.js → cli-4aj25szq.js} +2 -2
  25. package/dist/{cli-7gtvvpbh.js → cli-4tvgz99m.js} +7 -7
  26. package/dist/cli-503ya656.js +455 -0
  27. package/dist/{cli-cw9zck8j.js → cli-5f2hq128.js} +8 -4
  28. package/dist/{cli-s8eet254.js → cli-68bmme5p.js} +11078 -27833
  29. package/dist/{cli-ftfvggzx.js → cli-79aq9cgb.js} +794 -563
  30. package/dist/cli-7cxw8b60.js +14910 -0
  31. package/dist/cli-7kyk9jr3.js +22 -0
  32. package/dist/cli-7zttzetj.js +324 -0
  33. package/dist/{cli-y95wp3tg.js → cli-96qd5psa.js} +1 -1
  34. package/dist/{cli-g8djp6qe.js → cli-9yg65c1j.js} +22 -0
  35. package/dist/{cli-3gmcwfyx.js → cli-ardpzhjm.js} +5 -5
  36. package/dist/{cli-7s3syk22.js → cli-b6thtzac.js} +5 -5
  37. package/dist/{cli-00w2c66p.js → cli-bgv70kym.js} +13 -2
  38. package/dist/{cli-j55n64b6.js → cli-bv0a38v9.js} +163 -44
  39. package/dist/cli-ca2w7zv7.js +115 -0
  40. package/dist/cli-cpvsaqsw.js +333 -0
  41. package/dist/cli-cygyqj3b.js +754 -0
  42. package/dist/{cli-x24pshtq.js → cli-dvjpkkjd.js} +6 -6
  43. package/dist/{cli-5d10vd2m.js → cli-e0ndxhtk.js} +4 -4
  44. package/dist/{cli-4198qc9v.js → cli-e4xnc0qp.js} +15 -3
  45. package/dist/{cli-rjehvrts.js → cli-eg7cdg68.js} +8 -8
  46. package/dist/cli-ejpygnqz.js +3815 -0
  47. package/dist/{cli-fgqxwrpv.js → cli-fjtp3463.js} +1631 -127
  48. package/dist/{cli-zt1227k1.js → cli-fkqhbg8g.js} +5 -3
  49. package/dist/{cli-47m6dkd3.js → cli-g9df2m37.js} +409 -168
  50. package/dist/cli-gzyh0j0g.js +8455 -0
  51. package/dist/cli-hgqfttfc.js +183 -0
  52. package/dist/cli-hj65wsts.js +17755 -0
  53. package/dist/cli-j9z8mfx3.js +476 -0
  54. package/dist/{cli-aehm1rbq.js → cli-kc3dp84d.js} +1135 -61
  55. package/dist/{cli-h38x0qhg.js → cli-kdnehe0t.js} +4 -3
  56. package/dist/{cli-20cj37vt.js → cli-m4excttz.js} +136 -2216
  57. package/dist/{cli-8qmme6vd.js → cli-mg24hkt4.js} +2 -2
  58. package/dist/{cli-xhaeg2nc.js → cli-mzkqpxee.js} +1 -1
  59. package/dist/{cli-q67y4wzm.js → cli-phsaj4xq.js} +24 -2
  60. package/dist/{cli-pw9rj5t9.js → cli-pqetb83f.js} +1 -176
  61. package/dist/{cli-rj19hc34.js → cli-qjxy52w7.js} +1 -1
  62. package/dist/{cli-53sz8gkz.js → cli-qv4jv8bx.js} +1 -1
  63. package/dist/cli-qvnnx4ct.js +1051 -0
  64. package/dist/cli-rb9m6rpe.js +4980 -0
  65. package/dist/{cli-tt9k8r35.js → cli-rfezr056.js} +476 -909
  66. package/dist/{cli-ggm92wcn.js → cli-rgjprct3.js} +1 -0
  67. package/dist/{cli-9httdpyq.js → cli-rrqmqr1s.js} +1 -1
  68. package/dist/cli-rykfmgwg.js +102 -0
  69. package/dist/{cli-eetrvv91.js → cli-t3p77qk1.js} +1 -1
  70. package/dist/{cli-68w09sjw.js → cli-vn0qv9cj.js} +25 -88
  71. package/dist/{cli-987hg0aa.js → cli-vwbbaecp.js} +2 -2
  72. package/dist/cli-wd9hz40d.js +69 -0
  73. package/dist/{cli-j57qkrpq.js → cli-wgjx0q33.js} +13 -13
  74. package/dist/cli-xgpf00sk.js +2149 -0
  75. package/dist/{cli-mcnhgnn1.js → cli-ykk5jaax.js} +184 -26
  76. package/dist/{cli-aky8tctm.js → cli-z9x0sh1t.js} +3 -3
  77. package/dist/{cli-1hkrch3k.js → cli-zw65mab6.js} +5 -5
  78. package/dist/cli.js +38 -29
  79. package/dist/cloudflare-workers-ai-system-one-1sg586qv.js +46 -0
  80. package/dist/{codex-adapter-cnm8wk11.js → codex-adapter-jqfqqa21.js} +21 -14
  81. package/dist/{codex-hook-9hptmdhj.js → codex-hook-5eys3dya.js} +2 -2
  82. package/dist/{codex-session-runner-gfg547zj.js → codex-session-runner-zd341dsm.js} +21 -14
  83. package/dist/{commands-e3bn0p75.js → commands-vfsysz4s.js} +9 -8
  84. package/dist/{db-03v9kzr2.js → db-s3znhvbn.js} +15 -10
  85. package/dist/{dispatcher-nrmg8yht.js → dispatcher-5xn0c7rf.js} +32 -28
  86. package/dist/{dsh-adapter-k03nfqa6.js → dsh-adapter-894mgfvs.js} +3 -3
  87. package/dist/{e2b-93x1k0j5.js → e2b-ez07znam.js} +4 -3
  88. package/dist/execute-71ysgv35.js +965 -0
  89. package/dist/{google-generative-ai-bxmv50nc.js → google-generative-ai-1nfcdphp.js} +5 -4
  90. package/dist/{google-vertex-809af514.js → google-vertex-1tqcd7b1.js} +5 -4
  91. package/dist/{handlers-aqcedbyb.js → handlers-5w4jwycr.js} +35 -31
  92. package/dist/{hook-7vkskkre.js → hook-853bxrr6.js} +16 -13
  93. package/dist/{hook-ybvmrmhq.js → hook-z0hbxj85.js} +16 -13
  94. package/dist/{http-2eqa8t24.js → http-akc3wy3m.js} +600 -123
  95. package/dist/{index-f6cryxwe.js → index-4a5njnm1.js} +2 -59
  96. package/dist/{index-a3vn96n8.js → index-5sz16ksh.js} +187 -47
  97. package/dist/{index-c6zsv1bd.js → index-bjmjh9h4.js} +11 -11
  98. package/dist/{index-chgdkq36.js → index-cd151qkq.js} +32 -28
  99. package/dist/{index-x71bvq0p.js → index-k661dzzr.js} +33 -29
  100. package/dist/{index-1yks3qcr.js → index-ttpbhnjg.js} +32 -28
  101. package/dist/{keepalive-vtkf5n99.js → keepalive-8p9rpvz5.js} +11 -10
  102. package/dist/{lead-h3sp357x.js → lead-jp69z5h3.js} +49 -45
  103. package/dist/llama-cpp-classify-t2tccams.js +335 -0
  104. package/dist/{maintenance-jzeqtk3q.js → maintenance-fwx4ev0k.js} +12 -12
  105. package/dist/{mistral-conversations-zx0kn554.js → mistral-conversations-bhvwqcnk.js} +14 -17
  106. package/dist/{oauth-refresh-sweep-qp3dzmdp.js → oauth-refresh-sweep-cwcq1ctx.js} +10 -9
  107. package/dist/{onboard-tmsp0qbz.js → onboard-2b4q0t84.js} +6 -5
  108. package/dist/{openai-codex-responses-4vjqzvjp.js → openai-codex-responses-nekthgks.js} +21 -8
  109. package/dist/{openai-completions-9hjrqbw8.js → openai-completions-hxtzwj0s.js} +8 -11
  110. package/dist/{openai-responses-7b1pn0xt.js → openai-responses-w4r118w7.js} +22 -17
  111. package/dist/{opencode-adapter-x6x2da1z.js → opencode-adapter-zz7xvjj6.js} +6 -4
  112. package/dist/{openrouter-images-9f9ek4rw.js → openrouter-images-b6sqqjgn.js} +4 -6
  113. package/dist/{otel-impl-9mz73q65.js → otel-impl-g8rs4vwr.js} +1 -1
  114. package/dist/{pi-messages-txpjj39q.js → pi-messages-vxdt8v7j.js} +2 -1
  115. package/dist/{pi-mono-adapter-m6dvv0dz.js → pi-mono-adapter-n0me28wt.js} +29 -17
  116. package/dist/pricing-refresh-0v77385v.js +39 -0
  117. package/dist/{rbac-roles-c9wqz83x.js → rbac-roles-krjyazbj.js} +10 -9
  118. package/dist/{render-v2-gryrh9h8.js → render-v2-3wjsp9aw.js} +10 -9
  119. package/dist/runtime-ktjysacx.js +24 -0
  120. package/dist/{setup-q9720bt6.js → setup-yeqryke6.js} +6 -5
  121. package/dist/typesafe-system-one-96960jxp.js +27 -0
  122. package/dist/{virtual-modules-3htpgn8x.js → virtual-modules-dzdeq1sf.js} +21 -13
  123. package/dist/{work-objects-b76x7deb.js → work-objects-3pakpkw0.js} +35 -31
  124. package/dist/{worker-6vrxbv41.js → worker-85testgj.js} +49 -45
  125. package/openapi.json +863 -14
  126. package/package.json +9 -8
  127. package/src/be/db/agents.ts +20 -0
  128. package/src/be/db/tasks/human-free.ts +442 -0
  129. package/src/be/db/tasks/read.ts +56 -1
  130. package/src/be/db/tasks/write.ts +107 -11
  131. package/src/be/db.ts +89 -165
  132. package/src/be/harness-model-support.ts +182 -0
  133. package/src/be/human-free-drain.ts +67 -0
  134. package/src/be/migrations/182_task_human_free_flag.sql +63 -0
  135. package/src/be/migrations/183_human_free_reclassify_queue.sql +25 -0
  136. package/src/be/migrations/184_model_catalog.sql +120 -0
  137. package/src/be/model-catalog-store.ts +644 -0
  138. package/src/be/model-tier-keys.ts +31 -0
  139. package/src/be/model-tier-resolution.ts +489 -0
  140. package/src/be/model-validation.ts +112 -0
  141. package/src/be/models-catalog.ts +61 -7
  142. package/src/be/modelsdev-cache.json +82 -0
  143. package/src/be/modelsdev-cache.ts +2 -0
  144. package/src/be/pricing-refresh.ts +154 -12
  145. package/src/be/scripts/typecheck.ts +15 -0
  146. package/src/be/seed-scripts/catalog/task-failure-audit.ts +61 -9
  147. package/src/be/swarm-config-guard.ts +6 -0
  148. package/src/cli.tsx +10 -0
  149. package/src/commands/claude-managed-setup.ts +3 -1
  150. package/src/commands/runner.ts +76 -2
  151. package/src/extensions/contract-types.generated.ts +1 -1
  152. package/src/heartbeat/heartbeat.ts +64 -15
  153. package/src/heartbeat/templates.ts +2 -2
  154. package/src/http/agents.ts +49 -1
  155. package/src/http/index.ts +8 -0
  156. package/src/http/models-catalog.ts +357 -8
  157. package/src/http/poll.ts +45 -2
  158. package/src/http/schedules.ts +36 -6
  159. package/src/http/session-data.ts +2 -5
  160. package/src/http/stats.ts +6 -8
  161. package/src/http/tasks.ts +26 -1
  162. package/src/http/usage-cache.ts +56 -15
  163. package/src/http/workflows.ts +35 -8
  164. package/src/prompts/session-templates.ts +1 -0
  165. package/src/providers/claude-managed-models.ts +47 -161
  166. package/src/providers/codex-adapter.ts +4 -4
  167. package/src/providers/codex-models.ts +65 -198
  168. package/src/providers/reasoning-effort.ts +44 -140
  169. package/src/rbac/legacy-policy.ts +25 -0
  170. package/src/rbac/permissions.ts +10 -0
  171. package/src/scripts-runtime/sdk-allowlist.ts +4 -0
  172. package/src/scripts-runtime/types/stdlib.d.ts +15 -0
  173. package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
  174. package/src/scripts-runtime/types/swarm-sdk.d.ts +15 -0
  175. package/src/server.ts +6 -0
  176. package/src/slack/message-text.ts +56 -2
  177. package/src/slack/thread-buffer.ts +10 -2
  178. package/src/tests/agents-list-model-display.test.ts +4 -6
  179. package/src/tests/base-prompt.test.ts +3 -0
  180. package/src/tests/claude-adapter.test.ts +10 -0
  181. package/src/tests/claude-managed-adapter.test.ts +12 -18
  182. package/src/tests/codex-adapter.test.ts +2 -1
  183. package/src/tests/codex-gpt6-release.test.ts +66 -5
  184. package/src/tests/context-window.test.ts +7 -5
  185. package/src/tests/db-extraction-contract.test.ts +1 -0
  186. package/src/tests/db-task-read-contract.test.ts +5 -1
  187. package/src/tests/db-task-write-contract.test.ts +4 -1
  188. package/src/tests/docs-index-footprint.test.js +3 -3
  189. package/src/tests/explicit-model-validation.test.ts +332 -0
  190. package/src/tests/harness-model-support.test.ts +374 -0
  191. package/src/tests/heartbeat-checklist-config.test.ts +71 -0
  192. package/src/tests/heartbeat-checklist.test.ts +4 -2
  193. package/src/tests/heartbeat-orphan-resume.test.ts +48 -0
  194. package/src/tests/human-free-flag.test.ts +279 -0
  195. package/src/tests/human-free-oracle.ts +57 -0
  196. package/src/tests/human-free-reclassify.test.ts +669 -0
  197. package/src/tests/migration-046-budgets.test.ts +3 -3
  198. package/src/tests/model-catalog-migration.test.ts +134 -0
  199. package/src/tests/model-catalog-table.test.ts +249 -0
  200. package/src/tests/model-catalog-write-access.test.ts +387 -0
  201. package/src/tests/model-tier-resolution.test.ts +519 -0
  202. package/src/tests/models-catalog.test.ts +1 -1
  203. package/src/tests/multi-runtime-enabled-stats.test.ts +65 -1
  204. package/src/tests/package-publish.test.ts +137 -36
  205. package/src/tests/pricing-routes.test.ts +2 -2
  206. package/src/tests/rbac-engine.test.ts +42 -2
  207. package/src/tests/reasoning-effort.test.ts +109 -0
  208. package/src/tests/refresh-modelsdev-carryforward.test.ts +5 -5
  209. package/src/tests/runtime-model-catalog.test.ts +86 -0
  210. package/src/tests/script-connections.test.ts +5 -0
  211. package/src/tests/seed-scripts.test.ts +89 -0
  212. package/src/tests/session-costs.test.ts +14 -11
  213. package/src/tests/slack-message-text.test.ts +101 -0
  214. package/src/tests/slack-thread-buffer-context.test.ts +93 -0
  215. package/src/tests/task-cascade-fail.test.ts +148 -2
  216. package/src/tests/tool-annotations.test.ts +1 -1
  217. package/src/tests/usage-cache.test.ts +166 -0
  218. package/src/tests/workflow-executors.test.ts +5 -2
  219. package/src/tests/workflow-system-one-decision-review.test.ts +1021 -0
  220. package/src/tests/workflow-system-one-decision-save-warnings.test.ts +365 -0
  221. package/src/tests/workflow-system-one-decision.test.ts +2063 -0
  222. package/src/tools/model-catalog/index.ts +137 -0
  223. package/src/tools/schedules/create-schedule.ts +16 -1
  224. package/src/tools/schedules/patch-schedule.ts +17 -0
  225. package/src/tools/schedules/update-schedule.ts +17 -0
  226. package/src/tools/send-task.ts +14 -0
  227. package/src/tools/task-action.ts +12 -0
  228. package/src/tools/tool-config.ts +3 -1
  229. package/src/tools/workflows/create-workflow.ts +9 -2
  230. package/src/tools/workflows/patch-workflow-node.ts +17 -7
  231. package/src/tools/workflows/patch-workflow.ts +9 -2
  232. package/src/tools/workflows/update-workflow.ts +9 -2
  233. package/src/types.ts +30 -1
  234. package/src/utils/context-window.ts +47 -53
  235. package/src/utils/harness-cli-version.ts +115 -0
  236. package/src/utils/harness-model-error.ts +33 -0
  237. package/src/utils/runtime-model-catalog.ts +137 -0
  238. package/src/workflows/approval-resolution.ts +39 -0
  239. package/src/workflows/definition.ts +7 -0
  240. package/src/workflows/engine.ts +69 -1
  241. package/src/workflows/executors/agent-task.ts +9 -0
  242. package/src/workflows/executors/base.ts +53 -0
  243. package/src/workflows/executors/human-in-the-loop.ts +8 -8
  244. package/src/workflows/executors/index.ts +1 -0
  245. package/src/workflows/executors/registry.ts +2 -0
  246. package/src/workflows/executors/system-one-decision.ts +1232 -0
  247. package/src/workflows/executors/system-one-providers.ts +301 -0
  248. package/src/workflows/executors/workflow-llm.ts +13 -1
  249. package/src/workflows/readiness.ts +86 -0
  250. package/src/workflows/recovery.ts +12 -12
  251. package/src/workflows/resume.ts +23 -9
  252. package/templates/schedules/daily-blocker-digest/content.md +4 -5
  253. package/templates/skills/agent-browser/SKILL.md +6 -5
  254. package/templates/skills/agent-browser/content.md +6 -5
  255. package/templates/skills/heartbeat-runbook/config.json +1 -1
  256. package/templates/skills/heartbeat-runbook/content.md +2 -2
  257. package/templates/skills/swarm-scripts/SKILL.md +1 -1
  258. package/templates/skills/swarm-scripts/content.md +1 -1
  259. package/templates/skills/workflow-iterate/content.md +81 -1
  260. package/templates/workflows/claude-code-changelog-watch/content.md +2 -0
  261. package/templates/workflows/pr-review-status-sweep/content.md +2 -0
  262. package/dist/claude-sdk-session-t442qz6f.js +0 -39108
  263. package/dist/cli-0xjkj1j7.js +0 -61
  264. package/dist/cli-65hjmev5.js +0 -1141
  265. package/dist/cli-aw1jwp1r.js +0 -14075
  266. package/dist/cli-h967nb6r.js +0 -6603
  267. package/dist/cli-vapyk8sw.js +0 -20
  268. package/dist/cli-ws6rqycm.js +0 -247
  269. package/dist/pricing-refresh-v1cbr9te.js +0 -27
  270. package/src/providers/modelsdev-reasoning.json +0 -6453
  271. package/dist/{codex-login-x3jk4etn.js → codex-login-he9vrn90.js} +3 -3
  272. package/dist/{index-f24115ge.js → index-2wsdsnmm.js} +3 -3
@@ -1,21 +1,48 @@
1
1
  // Live model catalog for the UI model picker.
2
2
  //
3
- // Serves the slim in-memory projection of the models.dev payload maintained
4
- // by the pricing-refresh loop (`src/be/models-catalog.ts`). Read-only: the
5
- // catalog is refreshed server-side (boot + every 12h); the UI polls this
6
- // endpoint instead of relying on the build-time snapshot bundled into the SPA.
3
+ // GET serves the slim projection of the persistent `model_catalog` table
4
+ // merged with `model_catalog_overlay` (src/be/model-catalog-store.ts). The
5
+ // catalog refreshes server-side (boot + every 12h); POST .../refresh forces a
6
+ // refresh and PUT/DELETE .../overlay manage hand-verified overlay rows.
7
+ // See runbooks/model-catalog.md.
7
8
 
8
9
  import type { IncomingMessage, ServerResponse } from "node:http";
9
10
  import { z } from "zod";
10
- import { getModelsCatalog } from "../be/models-catalog";
11
+ import { getAgentById } from "../be/db";
12
+ import {
13
+ agentRanModel,
14
+ getAgentHarnessCliVersion,
15
+ listHarnessModelSupport,
16
+ recordHarnessModelSupport,
17
+ } from "../be/harness-model-support";
18
+ import {
19
+ deleteModelCatalogOverlay,
20
+ loadModelsCatalog,
21
+ upsertModelCatalogOverlay,
22
+ } from "../be/model-catalog-store";
23
+ import { previewModelTiers } from "../be/model-tier-resolution";
24
+ import { requestModelCatalogRefresh } from "../be/pricing-refresh";
25
+ import { can, type RbacPrincipal } from "../rbac";
26
+ import { MODEL_TIERS, ProviderNameSchema } from "../types";
27
+ import { isUnknownModelError } from "../utils/harness-model-error";
28
+ import { getRequestAuth } from "../utils/request-auth-context";
11
29
  import { route } from "./route-def";
12
- import { json } from "./utils";
30
+ import { jsonError } from "./utils";
13
31
 
14
32
  const CatalogModelSchema = z.object({
15
33
  id: z.string(),
16
34
  name: z.string().optional(),
17
- cost: z.object({ input: z.number().optional(), output: z.number().optional() }).optional(),
35
+ cost: z
36
+ .object({
37
+ input: z.number().optional(),
38
+ output: z.number().optional(),
39
+ cache_read: z.number().optional(),
40
+ cache_write: z.number().optional(),
41
+ })
42
+ .optional(),
18
43
  limit: z.object({ context: z.number().optional() }).optional(),
44
+ release_date: z.string().optional(),
45
+ status: z.string().optional(),
19
46
  reasoning: z.boolean().optional(),
20
47
  reasoning_options: z
21
48
  .array(z.object({ type: z.string(), values: z.array(z.string()).optional() }))
@@ -28,6 +55,61 @@ const CatalogProviderSchema = z.object({
28
55
  models: z.record(z.string(), CatalogModelSchema),
29
56
  });
30
57
 
58
+ const ReasoningOptionSchema = z.object({
59
+ type: z.string(),
60
+ values: z.array(z.string()).optional(),
61
+ });
62
+
63
+ const OverlayPricingSchema = z.object({
64
+ input: z.number().nonnegative().optional(),
65
+ output: z.number().nonnegative().optional(),
66
+ cache_read: z.number().nonnegative().optional(),
67
+ cache_write: z.number().nonnegative().optional(),
68
+ });
69
+
70
+ export const ModelCatalogRefreshResultSchema = z.object({
71
+ status: z.enum(["updated", "not-modified", "skipped-fresh", "skipped-cooldown", "error"]),
72
+ models: z.number(),
73
+ added: z.array(z.string()),
74
+ checkedAt: z.number().nullable(),
75
+ /** Set with `skipped-cooldown`: milliseconds until a forced refresh is accepted again. */
76
+ retryAfterMs: z.number().optional(),
77
+ error: z.string().optional(),
78
+ });
79
+
80
+ export const ModelCatalogOverlayBodySchema = z.object({
81
+ provider: z.string().min(1),
82
+ modelId: z.string().min(1),
83
+ name: z.string().optional(),
84
+ family: z.string().optional(),
85
+ releaseDate: z.string().optional(),
86
+ contextWindow: z.number().int().positive().optional(),
87
+ maxOutput: z.number().int().positive().optional(),
88
+ reasoning: z.boolean().optional(),
89
+ reasoningOptions: z.array(ReasoningOptionSchema).optional(),
90
+ pricing: OverlayPricingSchema.optional(),
91
+ status: z.string().optional(),
92
+ reason: z.string().min(1),
93
+ verifiedBy: z.string().optional(),
94
+ expiresWhenUpstreamMatches: z.boolean().optional(),
95
+ });
96
+
97
+ const OverlayEntrySchema = ModelCatalogOverlayBodySchema.extend({
98
+ name: z.string().nullish(),
99
+ family: z.string().nullish(),
100
+ releaseDate: z.string().nullish(),
101
+ contextWindow: z.number().nullish(),
102
+ maxOutput: z.number().nullish(),
103
+ reasoning: z.boolean().nullish(),
104
+ reasoningOptions: z.array(ReasoningOptionSchema).nullish(),
105
+ pricing: OverlayPricingSchema.nullish(),
106
+ status: z.string().nullish(),
107
+ verifiedBy: z.string().nullish(),
108
+ expiresWhenUpstreamMatches: z.boolean(),
109
+ createdAt: z.number(),
110
+ updatedAt: z.number(),
111
+ });
112
+
31
113
  const getCatalog = route({
32
114
  method: "get",
33
115
  path: "/api/models-catalog",
@@ -48,6 +130,192 @@ const getCatalog = route({
48
130
  },
49
131
  });
50
132
 
133
+ const ModelTierPreviewSchema = z.object({
134
+ provider: ProviderNameSchema,
135
+ tier: z.enum(MODEL_TIERS),
136
+ key: z.string(),
137
+ defaultValue: z.string(),
138
+ configured: z.string().nullable(),
139
+ source: z.enum(["tier-config", "tier-default"]),
140
+ resolvedModel: z.string().nullable(),
141
+ alias: z.string().nullable(),
142
+ });
143
+
144
+ const getTiers = route({
145
+ method: "get",
146
+ path: "/api/models-catalog/tiers",
147
+ pattern: ["api", "models-catalog", "tiers"],
148
+ summary: "Preview what each model tier resolves to per harness provider",
149
+ description:
150
+ "One row per provider and tier: the built-in default, the `MODEL_TIER_<PROVIDER>_<TIER>` value stored in swarm config (if any), which layer wins, and the concrete model it resolves to against the current catalog (`latest:` aliases resolved with the same soak and auto-upgrade rules as claim time, without recording a resolution). Ignores per-worker `MODEL_TIER_*` overrides and per-task models.",
151
+ tags: ["Pricing"],
152
+ responses: {
153
+ 200: {
154
+ description: "Tier previews",
155
+ schema: z.object({ tiers: z.array(ModelTierPreviewSchema) }),
156
+ },
157
+ },
158
+ });
159
+
160
+ const refreshCatalog = route({
161
+ method: "post",
162
+ path: "/api/models-catalog/refresh",
163
+ pattern: ["api", "models-catalog", "refresh"],
164
+ summary: "Refresh the model catalog from models.dev",
165
+ description:
166
+ "Unforced calls skip the network when the last models.dev check is younger than 4h (`skipped-fresh`). `force: true` always fetches, still conditional on the stored ETag (`not-modified` on 304), but a forced call within a minute of the previous one returns `skipped-cooldown` with `retryAfterMs`. Concurrent refreshes share one fetch. Lead agent or operator only. `added` lists provider/modelId keys new since the previous fetch.",
167
+ tags: ["Pricing"],
168
+ rbac: { permission: "models.catalog.write" },
169
+ body: z.object({ force: z.boolean().optional() }),
170
+ responses: {
171
+ 200: { description: "Refresh outcome", schema: ModelCatalogRefreshResultSchema },
172
+ 403: { description: "Forbidden" },
173
+ },
174
+ });
175
+
176
+ const upsertOverlay = route({
177
+ method: "put",
178
+ path: "/api/models-catalog/overlay",
179
+ pattern: ["api", "models-catalog", "overlay"],
180
+ summary: "Upsert one model-catalog overlay row",
181
+ description:
182
+ "Overlay facts win per non-null field over the models.dev row; overlay-only models are served too. Overlay prices fill pricing-table gaps (never override an active price). With `expiresWhenUpstreamMatches` (default true) the row is deleted once upstream matches every non-null fact.",
183
+ tags: ["Pricing"],
184
+ rbac: { permission: "models.catalog.write" },
185
+ body: ModelCatalogOverlayBodySchema,
186
+ responses: {
187
+ 200: { description: "Overlay row upserted", schema: OverlayEntrySchema },
188
+ 403: { description: "Forbidden" },
189
+ },
190
+ });
191
+
192
+ const deleteOverlay = route({
193
+ method: "delete",
194
+ path: "/api/models-catalog/overlay",
195
+ pattern: ["api", "models-catalog", "overlay"],
196
+ summary: "Delete one model-catalog overlay row",
197
+ tags: ["Pricing"],
198
+ rbac: { permission: "models.catalog.write" },
199
+ body: z.object({ provider: z.string().min(1), modelId: z.string().min(1) }),
200
+ responses: {
201
+ 200: { description: "Overlay row deleted", schema: z.object({ deleted: z.boolean() }) },
202
+ 403: { description: "Forbidden" },
203
+ },
204
+ });
205
+
206
+ const HarnessSupportRowSchema = z.object({
207
+ harness: z.string(),
208
+ cliVersion: z.string(),
209
+ modelId: z.string(),
210
+ status: z.enum(["ok", "unsupported", "unknown"]),
211
+ checkedAt: z.number(),
212
+ error: z.string().nullable(),
213
+ });
214
+
215
+ const listHarnessSupport = route({
216
+ method: "get",
217
+ path: "/api/models-catalog/harness-support",
218
+ pattern: ["api", "models-catalog", "harness-support"],
219
+ summary: "List harness CLI model support rows",
220
+ description:
221
+ "Whether a catalog model runs on a given `claude` / `codex` CLI version, as recorded by workers after a model's first run. No row means unknown (allowed at claim).",
222
+ tags: ["Pricing"],
223
+ query: z.object({ harness: z.string().optional(), cliVersion: z.string().optional() }),
224
+ responses: {
225
+ 200: {
226
+ description: "Support rows, newest first",
227
+ schema: z.object({ rows: z.array(HarnessSupportRowSchema) }),
228
+ },
229
+ },
230
+ });
231
+
232
+ const recordHarnessSupport = route({
233
+ method: "put",
234
+ path: "/api/models-catalog/harness-support",
235
+ pattern: ["api", "models-catalog", "harness-support"],
236
+ summary: "Record whether a harness CLI version accepts a model",
237
+ description:
238
+ "Written by workers: `ok` after a model's first successful run, `unsupported` when the CLI rejects the model id. Claim-time resolution falls back (alias/tier) or fails fast (explicit model) on `unsupported`.",
239
+ tags: ["Pricing"],
240
+ rbac: { permission: "models.harness-support.write" },
241
+ body: z.object({
242
+ harness: z.string().min(1).max(32),
243
+ cliVersion: z.string().min(1).max(64),
244
+ modelId: z.string().min(1).max(200),
245
+ status: z.enum(["ok", "unsupported", "unknown"]),
246
+ error: z.string().max(4000).optional(),
247
+ }),
248
+ responses: {
249
+ 200: { description: "Support row upserted", schema: HarnessSupportRowSchema },
250
+ 403: { description: "Forbidden" },
251
+ },
252
+ });
253
+
254
+ /**
255
+ * Resolve the caller and gate on `verb`; writes a 403 on denial. Refresh and overlay writes
256
+ * (`models.catalog.write`) are lead or operator only. Harness support (`models.harness-support.write`)
257
+ * is a registered agent's report about its own CLI (bound to it by `callerOwnsSupportRow`) or an
258
+ * operator correction. Dashboard users can do neither.
259
+ *
260
+ * Trust boundary: a bound identity (an `aswt_` user token or an `aseph_` session token) is honoured
261
+ * as is. Beyond that, the shared swarm key is the operator credential and `X-Agent-ID` is
262
+ * self-declared, the same model `config.ts`, `mcp-servers.ts` and `extensions.ts` use. Every
263
+ * worker holds that key, so this gate stops a mistaken or ordinary worker call, not a worker that
264
+ * chooses to drop or forge the header; that needs per-agent credentials swarm-wide.
265
+ */
266
+ async function ensureCatalogWriter(
267
+ req: IncomingMessage,
268
+ res: ServerResponse,
269
+ verb: "models.catalog.write" | "models.harness-support.write" = "models.catalog.write",
270
+ ): Promise<{ userId: string | null; agentId: string | null } | null> {
271
+ const auth = getRequestAuth(req);
272
+ const header = req.headers["x-agent-id"];
273
+ const agentId =
274
+ auth?.kind === "agent" ? auth.agentId : Array.isArray(header) ? header[0] : header;
275
+ let principal: RbacPrincipal;
276
+ if (auth?.kind === "user") {
277
+ principal = { kind: "user", userId: auth.userId };
278
+ } else if (agentId) {
279
+ // An X-Agent-ID wins over the shared API key: a worker holds that key, so treating it as the
280
+ // operator would let any worker skip the verb's lead-only rule.
281
+ const agent = await getAgentById(agentId);
282
+ principal = { kind: "agent", agentId, isLead: agent?.isLead ?? false };
283
+ } else if (auth?.kind === "operator") {
284
+ principal = { kind: "operator" };
285
+ } else {
286
+ principal = { kind: "agent", agentId: "", isLead: false };
287
+ }
288
+ const decision = can({
289
+ principal,
290
+ verb,
291
+ resource: { kind: "none" },
292
+ source: "http",
293
+ });
294
+ if (!decision.allow) {
295
+ jsonError(
296
+ res,
297
+ verb === "models.catalog.write"
298
+ ? "Writing the model catalog requires the lead agent or an operator"
299
+ : "Recording harness model support requires a registered agent or an operator",
300
+ 403,
301
+ );
302
+ return null;
303
+ }
304
+ return {
305
+ userId: auth?.kind === "user" ? auth.userId : null,
306
+ agentId: principal.kind === "agent" && principal.agentId ? principal.agentId : null,
307
+ };
308
+ }
309
+
310
+ async function callerOwnsSupportRow(
311
+ agentId: string,
312
+ body: { harness: string; cliVersion: string },
313
+ ): Promise<boolean> {
314
+ const agent = await getAgentById(agentId);
315
+ if (!agent || agent.harnessProvider !== body.harness) return false;
316
+ return (await getAgentHarnessCliVersion(agentId)) === body.cliVersion;
317
+ }
318
+
51
319
  export async function handleModelsCatalog(
52
320
  req: IncomingMessage,
53
321
  res: ServerResponse,
@@ -57,7 +325,88 @@ export async function handleModelsCatalog(
57
325
  if (getCatalog.match(req.method, pathSegments)) {
58
326
  const parsed = await getCatalog.parse(req, res, pathSegments, queryParams);
59
327
  if (!parsed) return true;
60
- json(res, getModelsCatalog());
328
+ getCatalog.respond(res, 200, await loadModelsCatalog());
329
+ return true;
330
+ }
331
+
332
+ if (getTiers.match(req.method, pathSegments)) {
333
+ const parsed = await getTiers.parse(req, res, pathSegments, queryParams);
334
+ if (!parsed) return true;
335
+ getTiers.respond(res, 200, { tiers: await previewModelTiers() });
336
+ return true;
337
+ }
338
+
339
+ if (refreshCatalog.match(req.method, pathSegments)) {
340
+ const parsed = await refreshCatalog.parse(req, res, pathSegments, queryParams);
341
+ if (!parsed) return true;
342
+ if (!(await ensureCatalogWriter(req, res))) return true;
343
+ refreshCatalog.respond(
344
+ res,
345
+ 200,
346
+ await requestModelCatalogRefresh({ force: parsed.body.force }),
347
+ );
348
+ return true;
349
+ }
350
+
351
+ if (upsertOverlay.match(req.method, pathSegments)) {
352
+ const parsed = await upsertOverlay.parse(req, res, pathSegments, queryParams);
353
+ if (!parsed) return true;
354
+ const writer = await ensureCatalogWriter(req, res);
355
+ if (!writer) return true;
356
+ const entry = await upsertModelCatalogOverlay(parsed.body, { userId: writer.userId });
357
+ upsertOverlay.respond(res, 200, entry);
358
+ return true;
359
+ }
360
+
361
+ if (deleteOverlay.match(req.method, pathSegments)) {
362
+ const parsed = await deleteOverlay.parse(req, res, pathSegments, queryParams);
363
+ if (!parsed) return true;
364
+ if (!(await ensureCatalogWriter(req, res))) return true;
365
+ const deleted = await deleteModelCatalogOverlay(parsed.body.provider, parsed.body.modelId);
366
+ deleteOverlay.respond(res, 200, { deleted });
367
+ return true;
368
+ }
369
+
370
+ if (listHarnessSupport.match(req.method, pathSegments)) {
371
+ const parsed = await listHarnessSupport.parse(req, res, pathSegments, queryParams);
372
+ if (!parsed) return true;
373
+ listHarnessSupport.respond(res, 200, { rows: await listHarnessModelSupport(parsed.query) });
374
+ return true;
375
+ }
376
+
377
+ if (recordHarnessSupport.match(req.method, pathSegments)) {
378
+ const parsed = await recordHarnessSupport.parse(req, res, pathSegments, queryParams);
379
+ if (!parsed) return true;
380
+ const writer = await ensureCatalogWriter(req, res, "models.harness-support.write");
381
+ if (!writer) return true;
382
+ if (writer.agentId && !(await callerOwnsSupportRow(writer.agentId, parsed.body))) {
383
+ // Support rows are shared per (harness, CLI version), so a worker may only write the row
384
+ // for the harness and CLI version it registered, never an arbitrary tuple.
385
+ jsonError(
386
+ res,
387
+ "Harness support can only be recorded for the calling agent's own harness and registered CLI version",
388
+ 403,
389
+ );
390
+ return true;
391
+ }
392
+ if (
393
+ writer.agentId &&
394
+ parsed.body.status === "unsupported" &&
395
+ !(
396
+ isUnknownModelError(parsed.body.error, parsed.body.modelId) &&
397
+ (await agentRanModel(writer.agentId, parsed.body.modelId))
398
+ )
399
+ ) {
400
+ // An `unsupported` row changes claims on every peer with this CLI, so an agent may only
401
+ // report it for a model it was assigned, with the CLI's model-rejection text.
402
+ jsonError(
403
+ res,
404
+ "An unsupported report needs a model the calling agent ran and the CLI's model-rejection error",
405
+ 403,
406
+ );
407
+ return true;
408
+ }
409
+ recordHarnessSupport.respond(res, 200, await recordHarnessModelSupport(parsed.body));
61
410
  return true;
62
411
  }
63
412
 
package/src/http/poll.ts CHANGED
@@ -28,6 +28,11 @@ import {
28
28
  upsertChannelActivityCursor,
29
29
  } from "../be/db";
30
30
  import { renderIdentity, resolveIdentity } from "../be/identity";
31
+ import {
32
+ parseModelTierOverridesHeader,
33
+ recordClaimModelResolution,
34
+ setAgentModelTierOverrides,
35
+ } from "../be/model-tier-resolution";
31
36
  import { touchRuntimeInstance } from "../be/multi-runtime";
32
37
  import { hasCapability } from "../server";
33
38
  import { fetchChannelActivity } from "../slack/channel-activity";
@@ -168,7 +173,15 @@ const pollTriggers = route({
168
173
  summary: "Poll for triggers (tasks, mentions)",
169
174
  tags: ["Poll"],
170
175
  auth: { apiKey: true, agentId: true },
171
- headers: runtimeInstanceHeader("poll for work"),
176
+ headers: runtimeInstanceHeader("poll for work").extend({
177
+ "X-Model-Tier-Overrides": z
178
+ .string()
179
+ .optional()
180
+ .describe(
181
+ "URL-encoded JSON {provider: {tier: model}} of the worker's MODEL_TIER_* env overrides. " +
182
+ "Stored on the agent row and applied at claim time (modelSource=worker-env).",
183
+ ),
184
+ }),
172
185
  responses: {
173
186
  200: { description: "Trigger data or null", schema: pollResponseSchema },
174
187
  400: { description: "Missing X-Agent-ID" },
@@ -230,6 +243,22 @@ async function attachmentsForTrigger(
230
243
  }));
231
244
  }
232
245
 
246
+ /**
247
+ * Claim-time model resolution (runbooks/model-tiers.md). Never blocks a claim:
248
+ * a resolution failure leaves the worker on its own local resolution.
249
+ */
250
+ async function claimModelFields(
251
+ task: { id: string; model?: string | null; modelTier?: string | null },
252
+ agent: Parameters<typeof recordClaimModelResolution>[1],
253
+ ): Promise<Awaited<ReturnType<typeof recordClaimModelResolution>>> {
254
+ try {
255
+ return await recordClaimModelResolution(task, agent);
256
+ } catch (error) {
257
+ console.warn(`[/api/poll] model resolution failed for task ${task.id}:`, error);
258
+ return {};
259
+ }
260
+ }
261
+
233
262
  // ─── Cursor Commit Endpoint ─────────────────────────────────────────────────
234
263
 
235
264
  const commitCursorsRoute = route({
@@ -268,6 +297,9 @@ export async function handlePoll(
268
297
  const runtimeInstanceId = ((h) => (Array.isArray(h) ? h[0] : h))(
269
298
  req.headers["x-runtime-instance-id"],
270
299
  );
300
+ const modelTierOverrides = parseModelTierOverridesHeader(
301
+ ((h) => (Array.isArray(h) ? h[0] : h))(req.headers["x-model-tier-overrides"]),
302
+ );
271
303
  // Handle cursor commit endpoint
272
304
  if (commitCursorsRoute.match(req.method, pathSegments)) {
273
305
  const parsed = await commitCursorsRoute.parse(req, res, pathSegments, queryParams);
@@ -311,6 +343,12 @@ export async function handlePoll(
311
343
  return { error: "Agent not found", status: 404 };
312
344
  }
313
345
 
346
+ // The worker's parsed MODEL_TIER_* env overrides ride on every poll so
347
+ // claim-time resolution can honor them (no write when unchanged).
348
+ if (modelTierOverrides) {
349
+ await setAgentModelTierOverrides(agent.id, modelTierOverrides);
350
+ }
351
+
314
352
  // Extension identities authenticate to the API but never execute
315
353
  // work: no offers, no pending assignments, no pool claims.
316
354
  if (isExtensionAgent(agent)) {
@@ -448,6 +486,7 @@ export async function handlePoll(
448
486
  taskId: pendingTask.id,
449
487
  task: {
450
488
  ...pendingTask,
489
+ ...(await claimModelFields(pendingTask, agent)),
451
490
  status: "in_progress",
452
491
  attachments: await attachmentsForTrigger(pendingTask.id),
453
492
  },
@@ -576,7 +615,11 @@ export async function handlePoll(
576
615
  trigger: {
577
616
  type: "task_assigned",
578
617
  taskId: claimed.id,
579
- task: { ...claimed, attachments: await attachmentsForTrigger(claimed.id) },
618
+ task: {
619
+ ...claimed,
620
+ ...(await claimModelFields(claimed, agent)),
621
+ attachments: await attachmentsForTrigger(claimed.id),
622
+ },
580
623
  ...(claimedRequestedBy && { requestedBy: claimedRequestedBy }),
581
624
  },
582
625
  };
@@ -16,6 +16,7 @@ import {
16
16
  updateScheduledTask,
17
17
  withFavoriteFlags,
18
18
  } from "../be/db";
19
+ import { explicitModelErrorForAgent } from "../be/model-validation";
19
20
  import { mergeScheduleTiming, validateRecurringTiming } from "../be/schedules/validate";
20
21
  import { getScript } from "../be/scripts/db";
21
22
  import { calculateNextRun, dispatchScheduleTarget } from "../scheduler/scheduler";
@@ -49,6 +50,8 @@ const scheduleUpdateBodySchema = z.object({
49
50
  timezone: z.string().optional(),
50
51
  model: z.string().nullable().optional(),
51
52
  modelTier: ModelTierSchema.nullable().optional(),
53
+ /** Accept a `model` the catalog does not list (a custom id). Without it an unknown id is a 400. */
54
+ allowCustomModel: z.boolean().optional(),
52
55
  nextRunAt: z.string().nullable().optional(),
53
56
  targetType: ScheduledTaskTargetTypeSchema.optional(),
54
57
  workflowId: z.string().uuid().nullable().optional(),
@@ -108,6 +111,8 @@ const createSchedule = route({
108
111
  timezone: z.string().optional(),
109
112
  model: z.string().optional(),
110
113
  modelTier: ModelTierSchema.optional(),
114
+ /** Accept a `model` the catalog does not list (a custom id). Without it an unknown id is a 400. */
115
+ allowCustomModel: z.boolean().optional(),
111
116
  scheduleType: z.enum(["recurring", "one_time"]).optional(),
112
117
  targetType: ScheduledTaskTargetTypeSchema.optional(),
113
118
  workflowId: z.string().uuid().optional(),
@@ -389,6 +394,16 @@ export async function handleSchedules(
389
394
  }
390
395
  }
391
396
 
397
+ const modelError = await explicitModelErrorForAgent({
398
+ model: splitLegacyModelAlias({ model: body.model, modelTier: body.modelTier }).model,
399
+ allowCustomModel: body.allowCustomModel,
400
+ agentId: body.targetAgentId,
401
+ });
402
+ if (modelError) {
403
+ jsonError(res, modelError, 400);
404
+ return true;
405
+ }
406
+
392
407
  try {
393
408
  const trustedUserId = await resolveHttpAuditUserId(req, myAgentId);
394
409
  let key: string | undefined;
@@ -535,23 +550,38 @@ export async function handleSchedules(
535
550
  throw error;
536
551
  }
537
552
  }
553
+ // The escape hatch is a request flag, not a schedule column. `body` aliases
554
+ // `parsed.body`, so read the flag before stripping it.
555
+ const allowCustomModel = parsed.body.allowCustomModel;
556
+ delete body.allowCustomModel;
557
+ const existing = await getScheduledTaskById(parsed.params.id);
558
+ if (!existing) {
559
+ jsonError(res, "Schedule not found", 404);
560
+ return true;
561
+ }
538
562
  if (parsed.body.model !== undefined || parsed.body.modelTier !== undefined) {
539
563
  const normalizedModel = splitLegacyModelAlias({
540
564
  model: parsed.body.model,
541
565
  modelTier: parsed.body.modelTier,
542
566
  });
567
+ // A model the schedule already stores is not re-judged, so an unrelated edit still saves.
568
+ if (normalizedModel.model !== existing.model) {
569
+ const modelError = await explicitModelErrorForAgent({
570
+ model: normalizedModel.model,
571
+ allowCustomModel,
572
+ agentId: parsed.body.targetAgentId ?? existing.targetAgentId,
573
+ });
574
+ if (modelError) {
575
+ jsonError(res, modelError, 400);
576
+ return true;
577
+ }
578
+ }
543
579
  if (parsed.body.model !== undefined) body.model = normalizedModel.model ?? null;
544
580
  if (parsed.body.modelTier !== undefined || normalizedModel.modelTier) {
545
581
  body.modelTier = normalizedModel.modelTier ?? null;
546
582
  }
547
583
  }
548
584
 
549
- const existing = await getScheduledTaskById(parsed.params.id);
550
- if (!existing) {
551
- jsonError(res, "Schedule not found", 404);
552
- return true;
553
- }
554
-
555
585
  // Reject updates on completed one-time schedules
556
586
  if (existing.scheduleType === "one_time" && !existing.enabled && existing.lastRunAt) {
557
587
  jsonError(res, "One-time schedule has already executed. Create a new one instead.", 400);
@@ -13,7 +13,6 @@ import {
13
13
  getSessionCostsFiltered,
14
14
  getSessionLogsByTaskId,
15
15
  getTaskById,
16
- getUsageDataVersion,
17
16
  } from "../be/db";
18
17
  import { recordSessionCost } from "../otel";
19
18
  import { incrementServerSessionsProcessed } from "../server-runtime-counters";
@@ -457,8 +456,7 @@ export async function handleSessionData(
457
456
  userId: parsed.query.userId || undefined,
458
457
  groupBy: parsed.query.groupBy || "both",
459
458
  } as const;
460
- const version = await getUsageDataVersion();
461
- const summary = await cachedUsageReport(`summary:${version}:${JSON.stringify(opts)}`, () =>
459
+ const summary = await cachedUsageReport(`summary:${JSON.stringify(opts)}`, () =>
462
460
  getSessionCostSummary(opts),
463
461
  );
464
462
  getSessionCostSummaryRoute.respond(res, 200, summary);
@@ -478,8 +476,7 @@ export async function handleSessionData(
478
476
  startDate: parsed.query.startDate || undefined,
479
477
  endDate: parsed.query.endDate || undefined,
480
478
  };
481
- const version = await getUsageDataVersion();
482
- const rows = await cachedUsageReport(`attribution:${version}:${JSON.stringify(opts)}`, () =>
479
+ const rows = await cachedUsageReport(`attribution:${JSON.stringify(opts)}`, () =>
483
480
  getAttributionByPerson(opts),
484
481
  );
485
482
  getAttributionByPersonRoute.respond(res, 200, { rows });
package/src/http/stats.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
3
  import {
4
- getAllAgents,
4
+ getAgentStatusCounts,
5
5
  getAllLogs,
6
6
  getAllServices,
7
7
  getConcurrentContext,
@@ -15,6 +15,7 @@ import { getDbRetentionStats } from "../be/db-retention";
15
15
  import { isSteeringEnabled } from "../be/steering";
16
16
  import type { AgentLog } from "../types";
17
17
  import { AgentLogSchema, ScheduledTaskSchema, ServiceSchema } from "../types";
18
+ import { isEnvFlagEnabled } from "../utils/env-flag";
18
19
  import { isMultiRuntimeEnabled } from "../utils/multi-runtime";
19
20
  import { resolveHttpFavoriteOwner } from "./favorite-owner";
20
21
  import { route } from "./route-def";
@@ -51,6 +52,7 @@ const DashboardStatsSchema = z.object({
51
52
  tasks: TaskCountsSchema,
52
53
  steeringEnabled: z.boolean(),
53
54
  multiRuntimeEnabled: z.boolean(),
55
+ devMode: z.boolean(),
54
56
  });
55
57
 
56
58
  /** Mirrors `DbRetentionTableStats` (src/be/db-retention.ts). */
@@ -247,16 +249,11 @@ export async function handleStats(
247
249
  }
248
250
 
249
251
  if (getStats.match(req.method, pathSegments)) {
250
- const agents = await getAllAgents();
252
+ const agents = await getAgentStatusCounts();
251
253
  const taskStats = await getTaskStats();
252
254
 
253
255
  const stats = {
254
- agents: {
255
- total: agents.length,
256
- idle: agents.filter((a) => a.status === "idle").length,
257
- busy: agents.filter((a) => a.status === "busy").length,
258
- offline: agents.filter((a) => a.status === "offline").length,
259
- },
256
+ agents,
260
257
  tasks: {
261
258
  total: taskStats.total,
262
259
  unassigned: taskStats.unassigned,
@@ -271,6 +268,7 @@ export async function handleStats(
271
268
  // Authenticated home for the steering feature flag — deliberately NOT on
272
269
  // the unauthenticated /health endpoint (config must not leak).
273
270
  steeringEnabled: isSteeringEnabled(),
271
+ devMode: isEnvFlagEnabled("SWARM_DEV_MODE", false),
274
272
  // Same authenticated home, for the same reason. Read through the helper
275
273
  // the server itself branches on, and read PER REQUEST rather than
276
274
  // captured at module load, so the reported value tracks a `swarm_config`