@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,374 @@
1
+ // Model-catalog phase 4: harness CLI model support, CLI-unsupported fallback
2
+ // and fail-fast at claim, and the worker-side outcome reporter.
3
+
4
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
5
+ import { unlink } from "node:fs/promises";
6
+ import { closeDb, createAgent, createTaskExtended, getDbClient, initDb } from "../be/db";
7
+ import {
8
+ getHarnessModelSupport,
9
+ recordHarnessModelSupport,
10
+ setAgentHarnessCliVersion,
11
+ } from "../be/harness-model-support";
12
+ import { type ModelCatalogEntry, replaceModelCatalog } from "../be/model-catalog-store";
13
+ import { invalidateTierResolutionCatalog } from "../be/model-tier-resolution";
14
+ import { handlePoll } from "../http/poll";
15
+ import {
16
+ parseCliVersion,
17
+ reportHarnessModelOutcome,
18
+ resetHarnessCliVersionForTests,
19
+ } from "../utils/harness-cli-version";
20
+ import { isUnknownModelError } from "../utils/harness-model-error";
21
+
22
+ const TEST_DB_PATH = "./test-harness-model-support.sqlite";
23
+ const DAY = 24 * 60 * 60 * 1000;
24
+ const CLI = "2.1.283";
25
+
26
+ async function removeDbFiles(path: string): Promise<void> {
27
+ for (const suffix of ["", "-wal", "-shm"]) {
28
+ try {
29
+ await unlink(path + suffix);
30
+ } catch (error) {
31
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
32
+ }
33
+ }
34
+ }
35
+
36
+ function entry(modelId: string, daysOld: number): ModelCatalogEntry {
37
+ return {
38
+ provider: "anthropic",
39
+ modelId,
40
+ releaseDate: new Date(Date.now() - daysOld * DAY).toISOString().slice(0, 10),
41
+ pricing: { input: 5, output: 25 },
42
+ checkedAt: Date.now(),
43
+ };
44
+ }
45
+
46
+ type Trigger = { type: string; taskId: string; task: Record<string, unknown> };
47
+
48
+ async function callPoll(agentId: string): Promise<Trigger | null> {
49
+ let bodyStr = "";
50
+ const req = {
51
+ method: "GET",
52
+ url: "/api/poll",
53
+ headers: { "x-agent-id": agentId },
54
+ } as unknown as Parameters<typeof handlePoll>[0];
55
+ const res = {
56
+ setHeader() {},
57
+ writeHead() {},
58
+ end(body?: string) {
59
+ bodyStr = body ?? "";
60
+ },
61
+ } as unknown as Parameters<typeof handlePoll>[1];
62
+ await handlePoll(req, res, ["api", "poll"], new URLSearchParams(), agentId);
63
+ return (JSON.parse(bodyStr) as { trigger: Trigger | null }).trigger;
64
+ }
65
+
66
+ async function worker(name: string) {
67
+ const agent = await createAgent({
68
+ name,
69
+ isLead: false,
70
+ status: "idle",
71
+ maxTasks: 1,
72
+ harnessProvider: "claude",
73
+ });
74
+ await setAgentHarnessCliVersion(agent.id, CLI);
75
+ return agent;
76
+ }
77
+
78
+ beforeAll(async () => {
79
+ await removeDbFiles(TEST_DB_PATH);
80
+ initDb(TEST_DB_PATH);
81
+ });
82
+
83
+ afterAll(async () => {
84
+ closeDb();
85
+ await removeDbFiles(TEST_DB_PATH);
86
+ });
87
+
88
+ beforeEach(async () => {
89
+ const client = getDbClient();
90
+ await client.run("DELETE FROM agent_tasks");
91
+ await client.run("DELETE FROM agents");
92
+ await client.run("DELETE FROM harness_model_support");
93
+ await client.run("DELETE FROM model_alias_resolutions");
94
+ await replaceModelCatalog([
95
+ entry("claude-opus-5", 200),
96
+ entry("claude-opus-5-5", 30),
97
+ entry("claude-sonnet-5-5", 10),
98
+ ]);
99
+ invalidateTierResolutionCatalog();
100
+ delete process.env.MODEL_TIER_CLAUDE_SMART;
101
+ });
102
+
103
+ describe("claim-time CLI support", () => {
104
+ test("alias resolving to an unsupported model falls back within the family", async () => {
105
+ process.env.MODEL_TIER_CLAUDE_SMART = "latest:anthropic/opus@stable";
106
+ await recordHarnessModelSupport({
107
+ harness: "claude",
108
+ cliVersion: CLI,
109
+ modelId: "claude-opus-5-5",
110
+ status: "unsupported",
111
+ error: "There's an issue with the selected model (claude-opus-5-5).",
112
+ });
113
+ const w = await worker("w-fallback");
114
+ const task = await createTaskExtended("smart work", { agentId: w.id, modelTier: "smart" });
115
+
116
+ const trigger = await callPoll(w.id);
117
+ expect(trigger?.task.resolvedModel).toBe("claude-opus-5");
118
+ expect(trigger?.task.modelSource).toBe("fallback:cli-unsupported");
119
+ const row = await getDbClient().get<{ resolvedModel: string; modelSource: string }>(
120
+ "SELECT resolvedModel, modelSource FROM agent_tasks WHERE id = ?",
121
+ [task.id],
122
+ );
123
+ expect(row).toEqual({
124
+ resolvedModel: "claude-opus-5",
125
+ modelSource: "fallback:cli-unsupported",
126
+ });
127
+ delete process.env.MODEL_TIER_CLAUDE_SMART;
128
+ });
129
+
130
+ test("the fallback never picks an unpriced sibling, which would run outside cost accounting", async () => {
131
+ process.env.MODEL_TIER_CLAUDE_SMART = "latest:anthropic/opus@stable";
132
+ // Newer than the priced fallback (claude-opus-5) but with no price yet.
133
+ await replaceModelCatalog([
134
+ entry("claude-opus-5", 200),
135
+ entry("claude-opus-5-5", 30),
136
+ { ...entry("claude-opus-5-4", 60), pricing: null },
137
+ ]);
138
+ invalidateTierResolutionCatalog();
139
+ await recordHarnessModelSupport({
140
+ harness: "claude",
141
+ cliVersion: CLI,
142
+ modelId: "claude-opus-5-5",
143
+ status: "unsupported",
144
+ });
145
+ const w = await worker("w-unpriced-fallback");
146
+ await createTaskExtended("smart work", { agentId: w.id, modelTier: "smart" });
147
+
148
+ const trigger = await callPoll(w.id);
149
+ expect(trigger?.task.resolvedModel).toBe("claude-opus-5");
150
+ expect(trigger?.task.modelSource).toBe("fallback:cli-unsupported");
151
+ delete process.env.MODEL_TIER_CLAUDE_SMART;
152
+ });
153
+
154
+ test("explicit unsupported model fails fast: trigger carries modelUnsupported", async () => {
155
+ await recordHarnessModelSupport({
156
+ harness: "claude",
157
+ cliVersion: CLI,
158
+ modelId: "claude-opus-5-5",
159
+ status: "unsupported",
160
+ });
161
+ const w = await worker("w-explicit");
162
+ await createTaskExtended("pinned work", { agentId: w.id, model: "claude-opus-5-5" });
163
+ const trigger = await callPoll(w.id);
164
+ expect(String(trigger?.task.modelUnsupported)).toContain("claude-opus-5-5");
165
+ expect(trigger?.task.resolvedModel).toBeUndefined();
166
+ });
167
+
168
+ test("an explicit latest: alias on an unsupported model falls back, like a tier alias", async () => {
169
+ await recordHarnessModelSupport({
170
+ harness: "claude",
171
+ cliVersion: CLI,
172
+ modelId: "claude-opus-5-5",
173
+ status: "unsupported",
174
+ });
175
+ const w = await worker("w-explicit-alias");
176
+ const task = await createTaskExtended("aliased work", {
177
+ agentId: w.id,
178
+ model: "latest:anthropic/opus",
179
+ });
180
+ const trigger = await callPoll(w.id);
181
+ expect(trigger?.task.modelUnsupported).toBeUndefined();
182
+ expect(trigger?.task.resolvedModel).toBe("claude-opus-5");
183
+ expect(trigger?.task.modelSource).toBe("fallback:cli-unsupported");
184
+ // The alias the task named is still on the record.
185
+ expect(trigger?.task.modelAlias).toBe("latest:anthropic/opus");
186
+ const row = await getDbClient().get<{ modelAlias: string }>(
187
+ "SELECT modelAlias FROM agent_tasks WHERE id = ?",
188
+ [task.id],
189
+ );
190
+ expect(row?.modelAlias).toBe("latest:anthropic/opus");
191
+ });
192
+
193
+ test("an alias with no usable sibling keeps its resolution instead of failing the task", async () => {
194
+ for (const modelId of ["claude-opus-5-5", "claude-opus-5"]) {
195
+ await recordHarnessModelSupport({
196
+ harness: "claude",
197
+ cliVersion: CLI,
198
+ modelId,
199
+ status: "unsupported",
200
+ });
201
+ }
202
+ const w = await worker("w-no-sibling");
203
+ await createTaskExtended("aliased work", { agentId: w.id, model: "latest:anthropic/opus" });
204
+ const trigger = await callPoll(w.id);
205
+ expect(trigger?.task.modelUnsupported).toBeUndefined();
206
+ expect(trigger?.task.resolvedModel).toBe("claude-opus-5-5");
207
+ expect(trigger?.task.modelSource).toBe("model");
208
+ });
209
+
210
+ test("a tier default (CLI shortname) the CLI rejected falls back within its family", async () => {
211
+ await recordHarnessModelSupport({
212
+ harness: "claude",
213
+ cliVersion: CLI,
214
+ modelId: "opus",
215
+ status: "unsupported",
216
+ });
217
+ const w = await worker("w-tier-default");
218
+ await createTaskExtended("smart work", { agentId: w.id, modelTier: "smart" });
219
+ const trigger = await callPoll(w.id);
220
+ expect(trigger?.task.modelUnsupported).toBeUndefined();
221
+ expect(trigger?.task.resolvedModel).toBe("claude-opus-5-5");
222
+ expect(trigger?.task.modelSource).toBe("fallback:cli-unsupported");
223
+ });
224
+
225
+ test("a tier default shortname stays when only a catalog id was rejected: the CLI resolves it", async () => {
226
+ await recordHarnessModelSupport({
227
+ harness: "claude",
228
+ cliVersion: CLI,
229
+ modelId: "claude-opus-5-5",
230
+ status: "unsupported",
231
+ });
232
+ const w = await worker("w-shortname-ok");
233
+ await createTaskExtended("smart work", { agentId: w.id, modelTier: "smart" });
234
+ const trigger = await callPoll(w.id);
235
+ expect(trigger?.task.resolvedModel).toBe("opus");
236
+ expect(trigger?.task.modelSource).toBe("tier-default");
237
+ });
238
+
239
+ test("a concrete id from a tier config value falls back; only a pinned task model fails fast", async () => {
240
+ process.env.MODEL_TIER_CLAUDE_SMART = "claude-opus-5-5";
241
+ await recordHarnessModelSupport({
242
+ harness: "claude",
243
+ cliVersion: CLI,
244
+ modelId: "claude-opus-5-5",
245
+ status: "unsupported",
246
+ });
247
+ const w = await worker("w-tier-config");
248
+ await createTaskExtended("smart work", { agentId: w.id, modelTier: "smart" });
249
+ const trigger = await callPoll(w.id);
250
+ expect(trigger?.task.resolvedModel).toBe("claude-opus-5");
251
+ expect(trigger?.task.modelSource).toBe("fallback:cli-unsupported");
252
+ delete process.env.MODEL_TIER_CLAUDE_SMART;
253
+ });
254
+
255
+ test("unknown support (no row) is allowed; other CLI versions are unaffected", async () => {
256
+ await recordHarnessModelSupport({
257
+ harness: "claude",
258
+ cliVersion: "2.0.0",
259
+ modelId: "claude-opus-5-5",
260
+ status: "unsupported",
261
+ });
262
+ const w = await worker("w-unknown");
263
+ await createTaskExtended("pinned work", { agentId: w.id, model: "claude-opus-5-5" });
264
+ const trigger = await callPoll(w.id);
265
+ expect(trigger?.task.resolvedModel).toBe("claude-opus-5-5");
266
+ expect(trigger?.task.modelUnsupported).toBeUndefined();
267
+ });
268
+
269
+ test("an ok row is never downgraded to unknown", async () => {
270
+ const base = { harness: "claude", cliVersion: CLI, modelId: "claude-opus-5-5" };
271
+ await recordHarnessModelSupport({ ...base, status: "ok" });
272
+ await recordHarnessModelSupport({ ...base, status: "unknown" });
273
+ expect(await getHarnessModelSupport("claude", CLI, "claude-opus-5-5")).toBe("ok");
274
+ });
275
+ });
276
+
277
+ describe("worker outcome reporting", () => {
278
+ test("parses CLI versions and recognizes unknown-model errors", () => {
279
+ expect(parseCliVersion("2.1.283 (Claude Code)")).toBe("2.1.283");
280
+ expect(parseCliVersion("codex-cli 0.157.1")).toBe("0.157.1");
281
+ expect(isUnknownModelError("There's an issue with the selected model (claude-x).")).toBe(true);
282
+ expect(isUnknownModelError("The model `gpt-9` does not exist or you do not have access")).toBe(
283
+ true,
284
+ );
285
+ expect(isUnknownModelError("rate limit reached")).toBe(false);
286
+ });
287
+
288
+ test("with a model id, only a rejection that names that model counts", () => {
289
+ const rejection = "There's an issue with the selected model (claude-x). It may not exist.";
290
+ expect(isUnknownModelError(rejection, "claude-x")).toBe(true);
291
+ expect(isUnknownModelError(rejection, "CLAUDE-X")).toBe(true);
292
+ expect(isUnknownModelError("The model `gpt-9` does not exist", "gpt-9")).toBe(true);
293
+ expect(isUnknownModelError("unknown model 'gpt-9'\nstack trace", "gpt-9")).toBe(true);
294
+ // Rejection text for another model, or phrase-only text from an unrelated failure.
295
+ expect(isUnknownModelError(rejection, "claude-y")).toBe(false);
296
+ expect(isUnknownModelError("tool call failed: invalid model output", "claude-x")).toBe(false);
297
+ expect(isUnknownModelError("unknown model type in schema\nran claude-x fine", "claude-x")).toBe(
298
+ false,
299
+ );
300
+ });
301
+
302
+ test("an unrelated failure containing a rejection phrase does not report unsupported", async () => {
303
+ resetHarnessCliVersionForTests({ claude: CLI });
304
+ const bodies: unknown[] = [];
305
+ const fetchImpl = (async (_url: string, init: RequestInit) => {
306
+ bodies.push(JSON.parse(String(init.body)));
307
+ return new Response("{}", { status: 200 });
308
+ }) as unknown as typeof fetch;
309
+
310
+ await reportHarnessModelOutcome({
311
+ apiUrl: "http://x",
312
+ agentId: "a",
313
+ harness: "claude",
314
+ fetchImpl,
315
+ model: "m1",
316
+ exitCode: 1,
317
+ failureReason: "Tool call failed: invalid model output from the summarizer",
318
+ });
319
+
320
+ expect(bodies).toEqual([]);
321
+ resetHarnessCliVersionForTests();
322
+ });
323
+
324
+ test("sends unsupported on a model rejection, ok once on success, nothing on other failures", async () => {
325
+ resetHarnessCliVersionForTests({ claude: CLI });
326
+ const bodies: unknown[] = [];
327
+ const fetchImpl = (async (_url: string, init: RequestInit) => {
328
+ bodies.push(JSON.parse(String(init.body)));
329
+ return new Response("{}", { status: 200 });
330
+ }) as unknown as typeof fetch;
331
+ const base = { apiUrl: "http://x", agentId: "a", harness: "claude", fetchImpl };
332
+
333
+ await reportHarnessModelOutcome({ ...base, model: "m1", exitCode: 1, failureReason: "boom" });
334
+ await reportHarnessModelOutcome({
335
+ ...base,
336
+ model: "m1",
337
+ exitCode: 1,
338
+ failureReason: "There's an issue with the selected model (m1).",
339
+ });
340
+ await reportHarnessModelOutcome({ ...base, model: "m2", exitCode: 0 });
341
+ await reportHarnessModelOutcome({ ...base, model: "m2", exitCode: 0 });
342
+
343
+ expect(bodies).toMatchObject([
344
+ { modelId: "m1", status: "unsupported", cliVersion: CLI },
345
+ { modelId: "m2", status: "ok" },
346
+ ]);
347
+ resetHarnessCliVersionForTests();
348
+ });
349
+
350
+ test("scrubs secrets from the failure text before it leaves the worker", async () => {
351
+ resetHarnessCliVersionForTests({ claude: CLI });
352
+ const token = "ghp_abcdefghijklmnopqrstuvwxyz0123456789";
353
+ const bodies: Array<{ error?: string }> = [];
354
+ const fetchImpl = (async (_url: string, init: RequestInit) => {
355
+ bodies.push(JSON.parse(String(init.body)));
356
+ return new Response("{}", { status: 200 });
357
+ }) as unknown as typeof fetch;
358
+
359
+ await reportHarnessModelOutcome({
360
+ apiUrl: "http://x",
361
+ agentId: "a",
362
+ harness: "claude",
363
+ fetchImpl,
364
+ model: "m1",
365
+ exitCode: 1,
366
+ failureReason: `There's an issue with the selected model (m1). token=${token}`,
367
+ });
368
+
369
+ expect(bodies).toHaveLength(1);
370
+ expect(bodies[0]?.error).toContain("selected model");
371
+ expect(bodies[0]?.error).not.toContain(token);
372
+ resetHarnessCliVersionForTests();
373
+ });
374
+ });
@@ -0,0 +1,71 @@
1
+ import { afterEach, describe, expect, spyOn, test } from "bun:test";
2
+ import { startHeartbeatChecklist, stopHeartbeatChecklist } from "../heartbeat/heartbeat";
3
+
4
+ // startHeartbeatChecklist reads HEARTBEAT_CHECKLIST_DISABLE and
5
+ // HEARTBEAT_CHECKLIST_INTERVAL_MS at call time. These cases pin how each raw
6
+ // env string is interpreted, observed through the startup log line.
7
+ const ENV_KEYS = ["HEARTBEAT_CHECKLIST_DISABLE", "HEARTBEAT_CHECKLIST_INTERVAL_MS"] as const;
8
+ const saved = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
9
+
10
+ function startWithEnv(env: Partial<Record<(typeof ENV_KEYS)[number], string>>): string[] {
11
+ for (const key of ENV_KEYS) {
12
+ if (env[key] === undefined) delete process.env[key];
13
+ else process.env[key] = env[key];
14
+ }
15
+ const spy = spyOn(console, "log").mockImplementation(() => {});
16
+ try {
17
+ startHeartbeatChecklist();
18
+ return spy.mock.calls.map((args) => String(args[0]));
19
+ } finally {
20
+ spy.mockRestore();
21
+ }
22
+ }
23
+
24
+ const DISABLED = "[Heartbeat] Checklist disabled via HEARTBEAT_CHECKLIST_DISABLE";
25
+ const TICK_OFF =
26
+ "[Heartbeat] Recurring checklist off (HEARTBEAT_CHECKLIST_INTERVAL_MS=0); boot triage still scheduled";
27
+ const starting = (ms: number) => `[Heartbeat] Checklist starting with ${ms}ms interval`;
28
+
29
+ describe("startHeartbeatChecklist env parsing", () => {
30
+ afterEach(() => {
31
+ stopHeartbeatChecklist();
32
+ for (const key of ENV_KEYS) {
33
+ if (saved[key] === undefined) delete process.env[key];
34
+ else process.env[key] = saved[key];
35
+ }
36
+ });
37
+
38
+ test.each([
39
+ ["unset", undefined],
40
+ ["empty", ""],
41
+ ["false", "false"],
42
+ ["0", "0"],
43
+ ["FALSE", "FALSE"],
44
+ ["typo falls back to on", "treu"],
45
+ ])("HEARTBEAT_CHECKLIST_DISABLE %s keeps the checklist on", (_label, value) => {
46
+ expect(startWithEnv({ HEARTBEAT_CHECKLIST_DISABLE: value })).toEqual([starting(1_800_000)]);
47
+ });
48
+
49
+ test.each([
50
+ ["true"],
51
+ ["1"],
52
+ ["TRUE"],
53
+ ])("HEARTBEAT_CHECKLIST_DISABLE %s turns the checklist off", (value) => {
54
+ expect(startWithEnv({ HEARTBEAT_CHECKLIST_DISABLE: value })).toEqual([DISABLED]);
55
+ });
56
+
57
+ test.each([
58
+ ["0", [TICK_OFF]],
59
+ ["-5", [TICK_OFF]],
60
+ ["60000", [starting(60_000)]],
61
+ ["abc", [starting(1_800_000)]],
62
+ ["", [starting(1_800_000)]],
63
+ ])("HEARTBEAT_CHECKLIST_INTERVAL_MS=%p", (value, expected) => {
64
+ expect(startWithEnv({ HEARTBEAT_CHECKLIST_INTERVAL_MS: value })).toEqual(expected);
65
+ });
66
+
67
+ test("interval 0: a second start does not schedule boot triage again", () => {
68
+ expect(startWithEnv({ HEARTBEAT_CHECKLIST_INTERVAL_MS: "0" })).toEqual([TICK_OFF]);
69
+ expect(startWithEnv({ HEARTBEAT_CHECKLIST_INTERVAL_MS: "0" })).toEqual([]);
70
+ });
71
+ });
@@ -290,8 +290,10 @@ describe("Heartbeat Checklist", () => {
290
290
  expect(tasks[0]!.task).toContain("≤10 items");
291
291
  expect(tasks[0]!.task).toContain("20 is the absolute max");
292
292
  expect(tasks[0]!.task).toContain("script-run");
293
- expect(tasks[0]!.task).toContain("Heartbeat Audit");
294
- expect(tasks[0]!.task).toContain("Rule #11");
293
+ expect(tasks[0]!.task).toContain("schedule-health");
294
+ expect(tasks[0]!.task).toContain("task-failure-audit");
295
+ expect(tasks[0]!.task).not.toContain("Heartbeat Audit");
296
+ expect(tasks[0]!.task).not.toMatch(/Rules? #\d/);
295
297
  });
296
298
 
297
299
  test("created task has correct tags", async () => {
@@ -24,6 +24,7 @@ import {
24
24
  runRebootSweep,
25
25
  setBeforeHeartbeatSupersedeForTests,
26
26
  } from "../heartbeat/heartbeat";
27
+ import { createResumeFollowUp } from "../tasks/worker-follow-up";
27
28
 
28
29
  const TEST_DB_PATH = "./test-heartbeat-orphan-resume.sqlite";
29
30
 
@@ -81,4 +82,51 @@ describe("Heartbeat orphaned supersede repair (TLA+ counterexample)", () => {
81
82
  expect((await getTaskById(task.id))?.status).toBe("superseded");
82
83
  expect(await getChildTasks(task.id)).toHaveLength(1);
83
84
  });
85
+
86
+ // Crash after createResumeFollowUp, before backfillSupersedeTaskResumeTaskId.
87
+ test("a dependent left waiting on a superseded task with a resume is re-pointed on the next sweep", async () => {
88
+ const agent = await createAgent({ name: "crashed-worker", isLead: false, status: "idle" });
89
+ const task = await createTaskExtended("Work interrupted mid-backfill", { agentId: agent.id });
90
+ const dependent = await createTaskExtended("Waits on the interrupted work", {
91
+ agentId: agent.id,
92
+ dependsOn: [task.id],
93
+ });
94
+ await startTask(task.id);
95
+
96
+ await supersedeTask(task.id, { reason: "crash_recovery", resumeTaskId: null });
97
+ const resume = await createResumeFollowUp({ parentId: task.id, reason: "crash_recovery" });
98
+ if (resume.kind !== "created") throw new Error(`resume not created: ${resume.kind}`);
99
+ await getDbClient().run("UPDATE agent_tasks SET finishedAt = ? WHERE id = ?", [
100
+ new Date(Date.now() - 10 * 60 * 1000).toISOString(),
101
+ task.id,
102
+ ]);
103
+
104
+ await runRebootSweep();
105
+ await codeLevelTriage();
106
+
107
+ const after = await getTaskById(dependent.id);
108
+ expect(after?.status).toBe("pending");
109
+ expect(after?.dependsOn).toEqual([resume.task.id]);
110
+ expect(await getChildTasks(task.id)).toHaveLength(1);
111
+ });
112
+
113
+ test("dependents of a superseded task that gets no resume are cascade-failed by the sweep", async () => {
114
+ const task = await createTaskExtended("Unassigned work superseded before any resume");
115
+ const dependent = await createTaskExtended("Waits on unassigned work", {
116
+ dependsOn: [task.id],
117
+ });
118
+
119
+ await supersedeTask(task.id, { reason: "manual", resumeTaskId: null });
120
+ expect((await getTaskById(dependent.id))?.status).toBe("unassigned");
121
+ await getDbClient().run("UPDATE agent_tasks SET finishedAt = ? WHERE id = ?", [
122
+ new Date(Date.now() - 10 * 60 * 1000).toISOString(),
123
+ task.id,
124
+ ]);
125
+
126
+ await codeLevelTriage();
127
+
128
+ const after = await getTaskById(dependent.id);
129
+ expect(after?.status).toBe("failed");
130
+ expect(after?.failureReason).toContain("was superseded");
131
+ });
84
132
  });