@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
@@ -0,0 +1,134 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { afterEach, describe, expect, test } from "bun:test";
3
+ import { runMigrations } from "../be/migrations/runner";
4
+
5
+ const DB_PATH = "./test-model-catalog-migration.sqlite";
6
+
7
+ async function removeDb(): Promise<void> {
8
+ for (const suffix of ["", "-wal", "-shm"]) {
9
+ try {
10
+ await Bun.file(DB_PATH + suffix).delete();
11
+ } catch (error) {
12
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
13
+ }
14
+ }
15
+ }
16
+
17
+ afterEach(removeDb);
18
+
19
+ function columnNames(db: Database, table: string): string[] {
20
+ return db
21
+ .query<{ name: string }, []>(`PRAGMA table_info(${table})`)
22
+ .all()
23
+ .map((column) => column.name);
24
+ }
25
+
26
+ function tableExists(db: Database, table: string): boolean {
27
+ return (
28
+ db
29
+ .query<{ name: string }, [string]>(
30
+ "SELECT name FROM sqlite_master WHERE type='table' AND name = ?",
31
+ )
32
+ .get(table) !== null
33
+ );
34
+ }
35
+
36
+ describe("migration 184 model catalog, tier resolution and harness support", () => {
37
+ test("upgrades a pre-184 database in place and keeps existing agent and task rows", async () => {
38
+ await removeDb();
39
+ const db = new Database(DB_PATH, { create: true });
40
+ try {
41
+ runMigrations(db);
42
+
43
+ // Roll the database back to the state before migration 184.
44
+ for (const table of [
45
+ "model_catalog",
46
+ "model_catalog_overlay",
47
+ "model_catalog_meta",
48
+ "model_alias_resolutions",
49
+ "harness_model_support",
50
+ ]) {
51
+ db.run(`DROP TABLE ${table}`);
52
+ }
53
+ for (const column of ["resolvedModel", "modelSource", "modelAlias"]) {
54
+ db.run(`ALTER TABLE agent_tasks DROP COLUMN ${column}`);
55
+ }
56
+ for (const column of ["modelTierOverrides", "harnessCliVersion"]) {
57
+ db.run(`ALTER TABLE agents DROP COLUMN ${column}`);
58
+ }
59
+ db.run("DELETE FROM _migrations WHERE version = 184");
60
+
61
+ db.run(
62
+ `INSERT INTO agents (id, name, status, createdAt, lastUpdatedAt)
63
+ VALUES ('agent-1', 'worker', 'idle', '2026-09-01', '2026-09-01')`,
64
+ );
65
+ db.run(
66
+ `INSERT INTO agent_tasks (id, task, status, createdAt, lastUpdatedAt)
67
+ VALUES ('task-1', 'existing task', 'pending', '2026-09-01', '2026-09-01')`,
68
+ );
69
+
70
+ runMigrations(db);
71
+
72
+ expect(
73
+ db
74
+ .query<{ version: number }, []>("SELECT version FROM _migrations WHERE version = 184")
75
+ .get(),
76
+ ).not.toBeNull();
77
+ for (const table of [
78
+ "model_catalog",
79
+ "model_catalog_overlay",
80
+ "model_catalog_meta",
81
+ "model_alias_resolutions",
82
+ "harness_model_support",
83
+ ]) {
84
+ expect(tableExists(db, table)).toBe(true);
85
+ }
86
+ expect(columnNames(db, "agent_tasks")).toEqual(
87
+ expect.arrayContaining(["resolvedModel", "modelSource", "modelAlias"]),
88
+ );
89
+ expect(columnNames(db, "agents")).toEqual(
90
+ expect.arrayContaining(["modelTierOverrides", "harnessCliVersion"]),
91
+ );
92
+
93
+ const task = db
94
+ .query<Record<string, unknown>, []>(
95
+ "SELECT task, resolvedModel, modelSource, modelAlias FROM agent_tasks WHERE id = 'task-1'",
96
+ )
97
+ .get();
98
+ expect(task).toEqual({
99
+ task: "existing task",
100
+ resolvedModel: null,
101
+ modelSource: null,
102
+ modelAlias: null,
103
+ });
104
+ const agent = db
105
+ .query<Record<string, unknown>, []>(
106
+ "SELECT name, modelTierOverrides, harnessCliVersion FROM agents WHERE id = 'agent-1'",
107
+ )
108
+ .get();
109
+ expect(agent).toEqual({ name: "worker", modelTierOverrides: null, harnessCliVersion: null });
110
+ } finally {
111
+ db.close();
112
+ }
113
+ });
114
+
115
+ test("harness_model_support only accepts ok, unsupported and unknown", async () => {
116
+ await removeDb();
117
+ const db = new Database(DB_PATH, { create: true });
118
+ try {
119
+ runMigrations(db);
120
+ const insert = (status: string) =>
121
+ db.run(
122
+ `INSERT INTO harness_model_support (harness, cliVersion, modelId, status, checkedAt)
123
+ VALUES ('claude', '2.1.0', ?, ?, 1)`,
124
+ [`m-${status}`, status],
125
+ );
126
+ expect(() => insert("ok")).not.toThrow();
127
+ expect(() => insert("unsupported")).not.toThrow();
128
+ expect(() => insert("unknown")).not.toThrow();
129
+ expect(() => insert("maybe")).toThrow(/CHECK/);
130
+ } finally {
131
+ db.close();
132
+ }
133
+ });
134
+ });
@@ -0,0 +1,249 @@
1
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { closeDb, getActivePricingRow, getDbClient, initDb } from "../be/db";
4
+ import {
5
+ listModelCatalogOverlay,
6
+ loadModelsCatalog,
7
+ markModelsCatalogStale,
8
+ upsertModelCatalogOverlay,
9
+ } from "../be/model-catalog-store";
10
+ import { getModelsCatalog, resetModelsCatalogForTests } from "../be/models-catalog";
11
+ import type { ModelsDevCache } from "../be/modelsdev-cache";
12
+ import { MODEL_CATALOG_FRESH_MS, refreshModelCatalog } from "../be/pricing-refresh";
13
+
14
+ const TEST_DB_PATH = "./test-model-catalog-table.sqlite";
15
+
16
+ async function removeDbFiles(path: string): Promise<void> {
17
+ for (const suffix of ["", "-wal", "-shm"]) {
18
+ try {
19
+ await unlink(path + suffix);
20
+ } catch (error) {
21
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
22
+ }
23
+ }
24
+ }
25
+
26
+ function payload(extraModels: Record<string, unknown> = {}): ModelsDevCache {
27
+ return {
28
+ openai: {
29
+ name: "OpenAI",
30
+ models: {
31
+ "gpt-base": {
32
+ name: "GPT Base",
33
+ cost: { input: 1, output: 4 },
34
+ limit: { context: 100_000 },
35
+ },
36
+ ...extraModels,
37
+ },
38
+ },
39
+ } as ModelsDevCache;
40
+ }
41
+
42
+ function ok(cache: ModelsDevCache, etag: string): typeof fetch {
43
+ return (async () =>
44
+ new Response(JSON.stringify(cache), {
45
+ status: 200,
46
+ headers: { "content-type": "application/json", etag },
47
+ })) as unknown as typeof fetch;
48
+ }
49
+
50
+ beforeAll(async () => {
51
+ await removeDbFiles(TEST_DB_PATH);
52
+ initDb(TEST_DB_PATH);
53
+ // Module state may carry over from other files sharing this process.
54
+ resetModelsCatalogForTests();
55
+ markModelsCatalogStale();
56
+ });
57
+
58
+ afterAll(async () => {
59
+ closeDb();
60
+ await removeDbFiles(TEST_DB_PATH);
61
+ });
62
+
63
+ afterEach(async () => {
64
+ const client = getDbClient();
65
+ await client.run("DELETE FROM model_catalog");
66
+ await client.run("DELETE FROM model_catalog_overlay");
67
+ await client.run("DELETE FROM model_catalog_meta");
68
+ await client.run("DELETE FROM pricing WHERE model LIKE 'gpt-%'");
69
+ resetModelsCatalogForTests();
70
+ markModelsCatalogStale();
71
+ });
72
+
73
+ describe("persistent model catalog", () => {
74
+ test("empty table serves the vendored snapshot", async () => {
75
+ const result = await loadModelsCatalog();
76
+ expect(result.source).toBe("snapshot");
77
+ expect(result.updatedAt).toBeNull();
78
+ });
79
+
80
+ test("forced refresh adds a new model to the catalog and pricing without restart", async () => {
81
+ await refreshModelCatalog({ force: true, now: 1_000, fetchImpl: ok(payload(), '"v1"') });
82
+
83
+ const result = await refreshModelCatalog({
84
+ force: true,
85
+ now: 2_000,
86
+ fetchImpl: ok(
87
+ payload({ "gpt-brand-new": { name: "Brand New", cost: { input: 3, output: 9 } } }),
88
+ '"v2"',
89
+ ),
90
+ });
91
+
92
+ expect(result.status).toBe("updated");
93
+ expect(result.added).toEqual(["openai/gpt-brand-new"]);
94
+ expect(result.models).toBe(2);
95
+ const catalog = await loadModelsCatalog();
96
+ expect(catalog.source).toBe("live");
97
+ expect(catalog.providers.openai?.models["gpt-brand-new"]?.cost?.input).toBe(3);
98
+ const price = await getActivePricingRow("codex", "gpt-brand-new", "input", 2_000);
99
+ expect(price?.pricePerMillionUsd).toBe(3);
100
+ });
101
+
102
+ test("304 keeps the catalog and bumps checkedAt", async () => {
103
+ await refreshModelCatalog({ force: true, now: 1_000, fetchImpl: ok(payload(), '"v1"') });
104
+ let sent: string | null = null;
105
+ const result = await refreshModelCatalog({
106
+ force: true,
107
+ now: 5_000,
108
+ fetchImpl: (async (_url: unknown, init?: RequestInit) => {
109
+ sent = new Headers(init?.headers).get("if-none-match");
110
+ return new Response(null, { status: 304 });
111
+ }) as unknown as typeof fetch,
112
+ });
113
+ expect(sent).toBe('"v1"');
114
+ expect(result.status).toBe("not-modified");
115
+ expect(result.models).toBe(1);
116
+ expect(result.checkedAt).toBe(5_000);
117
+ const row = await getDbClient().get<{ checkedAt: number }>(
118
+ "SELECT checkedAt FROM model_catalog WHERE modelId = 'gpt-base'",
119
+ );
120
+ expect(row?.checkedAt).toBe(5_000);
121
+ expect(getModelsCatalog().providers.openai?.models["gpt-base"]).toBeDefined();
122
+ });
123
+
124
+ test("unforced refresh within 4h skips the network", async () => {
125
+ await refreshModelCatalog({ now: 1_000, fetchImpl: ok(payload(), '"v1"') });
126
+ let called = false;
127
+ const fetchImpl = (async () => {
128
+ called = true;
129
+ return new Response(null, { status: 304 });
130
+ }) as unknown as typeof fetch;
131
+ const skipped = await refreshModelCatalog({ now: 1_000 + 60_000, fetchImpl });
132
+ expect(skipped.status).toBe("skipped-fresh");
133
+ expect(called).toBe(false);
134
+
135
+ const stale = await refreshModelCatalog({ now: 1_000 + MODEL_CATALOG_FRESH_MS + 1, fetchImpl });
136
+ expect(stale.status).toBe("not-modified");
137
+ expect(called).toBe(true);
138
+ });
139
+
140
+ test("overlay-only model appears in the catalog and gets a pricing row", async () => {
141
+ await refreshModelCatalog({ force: true, now: 1_000, fetchImpl: ok(payload(), '"v1"') });
142
+ await upsertModelCatalogOverlay(
143
+ {
144
+ provider: "openai",
145
+ modelId: "gpt-overlay-only",
146
+ name: "Overlay Only",
147
+ contextWindow: 400_000,
148
+ pricing: { input: 7, output: 21 },
149
+ reason: "launch blog",
150
+ },
151
+ { now: 2_000 },
152
+ );
153
+ const catalog = await loadModelsCatalog();
154
+ const model = catalog.providers.openai?.models["gpt-overlay-only"];
155
+ expect(model?.name).toBe("Overlay Only");
156
+ expect(model?.limit?.context).toBe(400_000);
157
+ const price = await getActivePricingRow("codex", "gpt-overlay-only", "output", 2_000);
158
+ expect(price?.pricePerMillionUsd).toBe(21);
159
+ });
160
+
161
+ test("a corrected overlay-only price supersedes the earlier pricing row", async () => {
162
+ await refreshModelCatalog({ force: true, now: 1_000, fetchImpl: ok(payload(), '"v1"') });
163
+ const overlay = {
164
+ provider: "openai",
165
+ modelId: "gpt-overlay-only",
166
+ pricing: { input: 7, output: 21 },
167
+ reason: "launch blog",
168
+ };
169
+ await upsertModelCatalogOverlay(overlay, { now: 2_000 });
170
+ await upsertModelCatalogOverlay(
171
+ { ...overlay, pricing: { input: 7, output: 28 } },
172
+ { now: 3_000 },
173
+ );
174
+ expect(
175
+ (await getActivePricingRow("codex", "gpt-overlay-only", "output", 2_500))?.pricePerMillionUsd,
176
+ ).toBe(21);
177
+ expect(
178
+ (await getActivePricingRow("codex", "gpt-overlay-only", "output", 3_000))?.pricePerMillionUsd,
179
+ ).toBe(28);
180
+ expect(
181
+ (await getActivePricingRow("codex", "gpt-overlay-only", "input", 3_000))?.effectiveFrom,
182
+ ).toBe(2_000);
183
+ });
184
+
185
+ test("an overlay price never overrides an upstream-priced model", async () => {
186
+ await refreshModelCatalog({ force: true, now: 1_000, fetchImpl: ok(payload(), '"v1"') });
187
+ const before = await getActivePricingRow("codex", "gpt-base", "output", 1_000);
188
+ expect(before).not.toBeNull();
189
+ await upsertModelCatalogOverlay(
190
+ {
191
+ provider: "openai",
192
+ modelId: "gpt-base",
193
+ pricing: { input: 99, output: 999 },
194
+ reason: "upstream wrong",
195
+ },
196
+ { now: 2_000 },
197
+ );
198
+ const after = await getActivePricingRow("codex", "gpt-base", "output", 2_000);
199
+ expect(after?.pricePerMillionUsd).toBe(before?.pricePerMillionUsd);
200
+ });
201
+
202
+ test("overlay wins per field over upstream", async () => {
203
+ await refreshModelCatalog({ force: true, now: 1_000, fetchImpl: ok(payload(), '"v1"') });
204
+ await upsertModelCatalogOverlay({
205
+ provider: "openai",
206
+ modelId: "gpt-base",
207
+ contextWindow: 200_000,
208
+ reason: "upstream wrong",
209
+ });
210
+ const model = (await loadModelsCatalog()).providers.openai?.models["gpt-base"];
211
+ expect(model?.limit?.context).toBe(200_000);
212
+ expect(model?.name).toBe("GPT Base");
213
+ });
214
+
215
+ test("overlay auto-expires once upstream matches", async () => {
216
+ await refreshModelCatalog({ force: true, now: 1_000, fetchImpl: ok(payload(), '"v1"') });
217
+ await upsertModelCatalogOverlay({
218
+ provider: "openai",
219
+ modelId: "gpt-late",
220
+ name: "Late",
221
+ pricing: { input: 2 },
222
+ reason: "not on models.dev yet",
223
+ });
224
+ await upsertModelCatalogOverlay({
225
+ provider: "openai",
226
+ modelId: "gpt-pinned",
227
+ name: "Pinned",
228
+ reason: "keep",
229
+ expiresWhenUpstreamMatches: false,
230
+ });
231
+
232
+ await refreshModelCatalog({
233
+ force: true,
234
+ now: 2_000,
235
+ fetchImpl: ok(
236
+ payload({
237
+ "gpt-late": { name: "Late", cost: { input: 2, output: 8 } },
238
+ "gpt-pinned": { name: "Pinned" },
239
+ }),
240
+ '"v2"',
241
+ ),
242
+ });
243
+
244
+ const remaining = (await listModelCatalogOverlay()).map((o) => o.modelId);
245
+ expect(remaining).toEqual(["gpt-pinned"]);
246
+ const late = (await loadModelsCatalog()).providers.openai?.models["gpt-late"];
247
+ expect(late?.cost?.output).toBe(8);
248
+ });
249
+ });